关于高效能java(practical java学习笔记)

来源:互联网 发布:java动态代理原理 编辑:程序博客网 时间:2024/06/07 21:40

写在最前面,本文中的所有内容根据practical java一书整理而成,仅作为个人笔记,更多详情请参详书中内容,请支持正版。ps:这是一本绝对的好书,非常值得很多次的看!!!!

    缺省情况下类的非私有方法和非static方法都是可以被子类重写的,当在一个类中的成员变量或成员方法设置为static,或者把类本身设置为static时,可对程序的运行期性能有显著提高。

    使用良好设计的多态,以避免使用instanceof(),当不得不使用instanceOf()时,如当从容器类去除具体的对象时,也可使用。

    当对象指针不再需要时,应将其设置成Null。尤其是在方法内部定义的局部变量,在使用完后即刻将其设置为null,以便gc能在下次运行时释放内存。而对于那些作用期与程序有效期等长的变量,则应该尽可能的小,大块头对象应该速生速灭。

    在有些垃圾回收机制的算法中,会在gc运行时挂起其他线程,这样做是确保一旦gc开始运转就可以拥有对堆(heap)的完整访问权限,可以安全完成任务而不受其他线程的威胁,一旦gc完成任务则会恢复此前挂起的所有线程。然而这样也潜在着在显示调用System.gc()时,存在因为gc的运行而导致的延迟,延迟的程度取决于JVM采用的垃圾回收算法。大多数JVMs的垃圾回收都有充足的运行,因此实在不必显示的运行。然而当程序中存在一些期望在继续运行时释放(回收)的所有可能的内存时,则可以考虑调用System.gc()。

    关于java中存储数据的几个存储区域:1.寄存器,存在于处理器内部,存储数量非常有限,速度最快。寄存器是编译器根据需要分配的,不能直接控制,并且也不能在自己的程序中找到寄存器内的任何踪迹。2.堆栈,存在于RAM内存中,处理速度非常快,仅次于寄存器。创建程序时,java必须知道堆栈中存储数据的具体长度和存活的具体时间,以便于分配内存。对象句柄保存在堆栈中,而java对象并不保存于此,基本数据类型保存在堆栈中。3.堆(heap),存在于RAM内存中,处理速度快,编译器并不知道要从堆中分配多少空间,也不知道数据要存活多长时间。java对象便保存于此。4.静态存储,存在于内存中的固定位置。程序运行期间,静态存储的数据随时等待调用。static指定对象的元素存在静态存储中,而java对象永远不会存在于静态存储中。5.常数存储,通常至于程序代码内部,永远不会改变。6.非RAM存储。主要的例子为“流式对象”和“固定对象”。固定对象保存在硬盘中,即使程序停止运行,他们的状态也不会变。

    java中所有对象都是通过object references—— 一种某种形式的指针(存放在堆栈stack中),来访问的,该指针存在于堆栈中,指向一个存放在对heap中的一个对象。下图演示的是一个int型变量i,与Integer对象j存储空间的结构。java中对对象的赋值操作实质上都是对指针的操作。

  

    关于“相等”,基本数据类型的比较用的是==,String等类类型用的是equals()。在判断两个对象是否相等使用equals()时,须先确认所比较的属性是否提供了自己的equals()方法(区别与Object的equals()方法),系统中自带的一些类,如String,Integer等,有自己的equals()方法,若没有,则须事先该属性所属类的equals()方法及其超类中的equals()方法。当Base class采用instanceof实现equals()而Derived class没有实现equals()时,有可能返回true,并且这种相等是对称的。如果base class和derived class都采用instanceof实现了equals(),就无法保持相等动作的对称性。在java.lang.Object以外的base class的equals()方法中,写instanceof检验比较合适,适当时候可以调用super.equals()。具体的设计根据需求来决定,尤其是在涉及到继承的equals()中,需考虑基类和子类中需要比较的各个属性。

    异常部分,如果同时存在catch区段和finally区段,程序控制会先跳到catch区段,然后跳到finally区段,如果没有catch区段,则程序控制会直接跳至finally区段。在处理先前的抛出的异常时,如果在catch区段或者finnaly区段又抛出异常,某些异常可能会被掩盖,只剩最后生成的异常才会被传递到调用端(对于此类问题,如果是自己建立的Exception子类的话,可以在子类中添加一个容器类,用来存放抛出的异常)。throw字句用来列出某个函数可能传至外界的所有可能异常,编译器强迫你必须捕捉这些异常,否则就用throw字句抛出这些异常。在开发的时候如果碰到一种工蜂函数(worker method),所谓工蜂函数是指那些为其他方法调用而准备的函数,完成共通性的任务,在很多其他函数中被调用,这种情况对工蜂函数中的异常通常有两种选择:1.在工蜂函数中捕获异常,并处理它;2.在工蜂函数中抛出异常,让调用者处理它。第一种可以阻止异常的进一步传递,而在没法做到正确处理时采用第二种方法时应该在设计之初就throw(throws)它(们),当一个函数中抛出多个异常(抛出一个异常的多个派生异常时,使用throws更合适)。对于一个自己定义的异常类,如果这个异常类(父类)再派生出其他异常(子类),则这些派生异常(子类)必须和这个异常(父类)的异常是同一类型或者是更加细化的异常类型。如果重写(overridding)的某个函数中没有跑出异常,则复写的函数中必须捕获异常,并就地处理异常。finally语句是java处理异常中的最大的特色,由于finally区段的代码总是会被执行,而不管是否抛出异常,finally在维护对象的内部状态和清理non-memery资源方面,尤其适用。try字句:只要存在finally区段就一定会执行,try区段一旦程序控制权离开就会进入finally区段,除非在try段离开后直接退出系统,比如System.exit(),如果没有退出系统的话,控制流跳出try后就会进入finally区段,不管try区段中有没有break,continue,return等语句,所以在try区段中尽量不要使用上述语句。异常可能会对代码性能产生负面影响。至于是否影响与代码的组织结构和JVM是否在运行期是否使用JIT(just in time)编译器相关(默认使用),所以尽量把try...catch语句写在循环之外。同时,不要将异常用于流程控制。构造函数同样可以抛出异常,不过最后在try之外构造一个类的空对象null,避免构造失败对象出现不可用状态。不要以为throw(throws),try..catch,finally是异常控制的起点和重点,其实它们只是异常控制的起点,真正困难的是将对象回复成有效状态。抛出异常的浅层目的是使将已经发生的问题通知系统的其他部分,更进一步的目标是使系统能够从问题中抽身恢复(recover)而保持正常运行状态,同时对于一些函数的行为属于“事物”型时,要做好失败时在异常捕获中采取相应的回滚措施。

    性能部分:高效的代码与良好的设计、明智地选择数据结构、明智地选择算法密切相关,远大于与实现语言的关系。JITs是针对相对较少的运行时间来设计的,因为它们的存在目的是加快代码的运行速度,而不是使代码慢下来。为了收集“充分的,为执行优化而必要的”数据,必须花费额外的时间。

    对象构建过程中的固定顺序:1.从heap中分配内存,用以存放全部的instance以及这个对象连同其superclass的实现专属数据(implementation-specific data),所谓“实现专属数据”包括指向“class and method data”的指针;2.对象的instance变量初始化及其相应的缺省值;3.调用most derived class(最深层派生类)的构造函数(constructor)。构造函数的第一件事就是调用superclass的构造函数。这个过程会一直反复持续到java.lang.Object的构造函数被调用为止。一定要记住java.lang.Object是一切java对象的base class。4.构造函数体执行之前,所有instance变量的初始值设定式(initializer)和初始化区段(initialization blocks)先获得执行,然后才执行构造函数本体。于是base class的构造函数最先执行,most derived class的构造函数最后获得执行。这使得任何class的构造函数都能放心大胆的调用其任何super class的instance变量。

    在着手性能提升的时候,一下特征会提高对象的构造成本:1.构造函数中拥有大量代码;2.内含众多数据或体积庞大的对象3.太深的继承层次。如果通过性能评测确定性能问题是因重型对象的构造而构成,将有以下可能选择:1.使用延迟求值技术(lazy evaluation);2.重新设计这个类,使之瘦身;3.如果引发性能问题的那些代码只使用了重型对象的某些部分,可以将这个类分解成为多个轻型类,并对性能需求最高的代码只是用轻型类。

    synchronized关键字将会产生monitorenter和monitorexit的bytecode,同时会对资源进行加锁和异常处理,所以synchronized会使函数变慢。如果不得不使用同步,请优先选择函数修饰符,而不是在函数内部使用synchronized区段。

    一些常用的策略:(1)循环内不变的部分转移到循环之外。(2)谨慎创建不必要的对象。(3)限制同步函数的使用,提供一个subclass,内涵那些函数的unsynchronized版本或者使用另一个class,提供unsynchronized版本。(4)尽可能使用stack变量,如果在循环之内访问static变量或者instance变量,则可以将他们存储在一个local stack中。(5)使用static、final和private函数来促成inlining。为了让函数成为inlining候选者,必须将它们声明成为static、final和private,此类函数可以在编译期被静态协议(statically resolved),而不是动态协议(dynamic resolution)。以函数主体(method body)代替函数调用(method call)会使函数执行更快。由于大型函数的inlining会使函数提及变得膨胀,所以只有小型函数才会考虑inlining,如可在getter和setter方法前面加上final修饰词。inlined函数只有在“多次调用”的情况下才会有令人侧目的性能提升,在第一次被调用后调用的越多,节省的越多。如果inlined函数有很多的调用点,.class文件体积会变大,因为原本只需要存储一遍的函数码由于inlined在所有调用点被复制一次。(6)instance变量初始化一次就好。java对于instance变量,static变量和arrays都进行缺省的初始化,而对于local变量没有缺省值,因此对于local变量必须明确的进行初始化。(7)使用基本类型使代码更快更小。(8)不要使用enumeration或者iterator来遍历vector。java中提供了对vector的四种遍历方法,分别为:Iterator,ListIterator,Enumeration和get函数,其中速度最快的是get函数,因为其在遍历时做的事情最少,get函数是synchronized。(8)使用System.arrayCopy()来复制arrays。(9)优先使用array,然后才考虑Vector和ArrayList。java底层中Vector是用array来实现的,而ArrayList其实是一种unsynchronized版本的Vector。当array的长度超出预定长度时,Vector和ArrayList都会采用自己的策略进行扩充,而当从Vector和ArrayList中删除一个元素时,其后的元素都会让前移一位(增加同理)。(10)尽可能少用对象,如果你调用的函数保存的是object reference,那么就不能使用这份技术。(11)使用延迟计算(加载)技术,即免除非必须的工作。

    多线程部分:对于instance函数,关键词synchronized其实并没有锁定函数或者代码,它锁定的是对象,而每个对象只有一个锁(lock)与之相关联。当synchronized被用作函数修饰符的时候,它所取得的lock将会被交给函数调用者(某个对象)。如果synchronized用于object reference,则取得的lock将会交给该reference所指的对象。同步的规则——同步机制锁定的是对象,而不是函数或者代码。JAVA中不允许将构造函数声明为synchronized,原因在于当两个线程并发调用同一构造函数时,它们各自操控的是属于同一个class的不同实体对象的内存,然而如果在这些构造函数内包含了彼此竞争共享资源的代码,则必须同步控制这些资源已回避这些冲突。当调用一个synchronized static函数时,获得的lock将与“定义该函数”的class的Class对象相关联(区别于与调用那个对象相关联);当对class literal调用其synchronized区段时,如synchronized(Test.class),获得的同样也是那个lock,即与特定Class相关联的lock(在没有共享资源的情况下,即便是同一个class的实例对象的锁与Class对象的锁也不是同一个,两个lock并不会互相排斥)。(12)以private数据+访问函数替代public/protected数据,要想完全保护synchronized函数中的数据,必须使这些数据成为class的private成员,避免数据遭讹用,并且使class具备多线程安全性,对于返回object reference的函数可以在函数内部提供这一object reference的克隆对象,并返回此克隆对象。如果某个synchronized函数调用了某个unsynchronized instance函数来修改对象,它是线程安全的,因为一旦unsynchronized函数被synchronized函数调用,它也会变成synchronized。如果synchronized直接修改的对象并非这个函数所属的class的private instance函数,那么程序就不具备多线程安全性。(13)java语言允许线程保存对象的私有专用副本,这样可以使线程更加高效的执行,当线程读写这些变量时,它们操作的是“私有专用副本”,而非主内存里的“主本”,“私有专用副本”只有在特定的同步点才回与主内存中的“正本”进行一致化动作。(14)以固定而全局性的顺序取得多个locks,以避免死锁。避免死锁的另一个方法是——将锁定顺序嵌入到对象内部(即在类中新建一个static变量作为计数器,每生成一个类对象计数器加一,根据对象的计数器值进行比较,从而得知类的顺序)。(15)只要代码在等待某个特定条件,它就应该在“一个循环内”,或谓“旋锁”spin lock。(16)不要对locked object即上锁对象的object reference重新赋值,以防引起锁定对象在object reference重新赋值后锁定对象不一致。(17)通过线程之间的协作,而非suspend()或者stop()来终止进程。

当构造函数调用non-final函数时有可能发生错误。如果该函数被一个derived class复写,而该函数返回一个在“instance变量初始化期间”被初始化的值。

class Base{
    private int val;
    Base(){
        val = lookup();
    }
    public int lookup(){
        return 5;
    }
    public int value(){
        return val;
    }
}
class Derived extends Base{
    private int num = 10;
    public int lookup() {
        return num;
    }
}
public class TestBaseAndDerived {
    public static void main(String[] args) {
        Derived der = new Derived();
        System.out.println("From main() d.value = " + der.value());

    }

}
程序运行结果为d.value=0,在Derived调用其构造函数时自动调用Base的构造函数,在Base的构造函数中再调用Derived对象的lookup()方法返回num,而此时的num还没有被赋值为10,仍然是默认的初始值0,然后函数调用返回,程序运行结束。参数的默认初始化发生在构造函数调用之前

原创粉丝点击