java优化手册.docx
《java优化手册.docx》由会员分享,可在线阅读,更多相关《java优化手册.docx(11页珍藏版)》请在冰豆网上搜索。
java优化手册
1.底层篇
1.1.基本数据类型时间分析
int的运算速度最快,short次之,byte再次之,long再次之。
float和double运算速度最慢。
除法比乘法慢的太多,基本上除法是乘法的9倍时间。
当然,除了浮点型外。
根据intelcpu的参考数据,乘法计算时间是移位运算的4-5倍是比较正常的。
long类型的计算很慢,建议一般少使用它。
double运算速度和float相当;
浮点的乘法比除法要慢。
但是,这个结果并不能真正说明问题。
这个结果只是一个一般性的,在特殊情况下,乘法还是比除法快,比如:
floatA*floatB仍然是比floatA/(1/floatB)快。
从实际的数据结果来讲,乘法的时候,乘数越小速度越快,特别是在乘数比3小的时候,乘法时耗接近20,大于4的时候,几乎都是600的时耗。
除法恰好相反,除数大于1的时候,时耗一般都是350,可是,当除数小于1的时候,时耗就变成了700了。
对于大家关心的移位和乘除2的问题,jdk5.0已经做了部分处理。
即“var*=2”和“var<<=1”耗费一样。
但是,除法并没有进行这类处理,即“var/=2”耗费和基本的除法一样。
1.2.类和接口调用时间分析
1.2.1.类的创建
虽然面向对象思想已经深入人心,但他在带来快捷方面的编程风格的时候,也带来了低下的效率。
在Java中,反应最快的是Object类(这也是显然的),建立一个新的Object类时耗仅仅为20单位。
而一个空类(即没有声明任何Methods和Fields)的建立时间则增加到了惊人的400单位。
如果再给类增加一些字段的话,时间耗费并没有特别大的增加,每增加一个int类型字段大概增加30个单位。
仅仅就创建时间来说,内嵌的类型都有不错的表现。
比如,创建一个int数组(仅仅包含一个元素)的时间只比创建一个Object对象的时间多一倍。
当然,如果你创建的数组对象包含1000个元素的话,其创建时间显然还会加上内存管理的时间了,它的时间大概是1万个时间单位。
请注意,我们这里讨论的时间单位其实十分小,1万个时间单位也仅仅只是有0.006毫秒(0.000006秒)。
创建一个byte、short、int、long、float和double数组对象的时间消耗几乎是一样的。
1.2.2.方法的调用
Java在这个方面有一点做得很好,就是调用一个只有很少量代码的方法的时耗和直接把这段代码写到本地的时耗相差很小。
当然不包括需要分配很多本地变量的情况。
调用本类(this指针)的方法是最快的,时间在1-2个单位。
调用其它类的静态方法也很快,速度和调用本来方法差不多。
调用其它类的非静态方法速度就慢一些,在1.5-2.5个时间单位之间。
调用继承接口的方法是十分缓慢的,是调用普通方法的3倍。
但是,如果在实现接口的时候加上final关键字的话,调用这个方法的时耗就和普通方法差不多了。
最慢的是已经同步化了的方法。
即加上了synchronized关键字的方法。
调用它的时耗比普通方法高出了近20倍。
如果不是万不得已,不要把synchronized加到方法上面,实在不行的话,你可以把它加到代码块上去,这种加法比直接加到方法上面快一点。
注意,因为方法大部分时候都是完成很多事情的,所以,十分注意调用方法的开销是没有必要的,因为这个时间和方法执行需要的时间比较起来只是毛毛雨。
1.3.基本操作时间耗费
2.通用篇
“通用篇”讨论的问题适合于大多数Java应用。
2.1.不用new关键词创建类的实例
用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。
但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。
clone()方法不会调用任何类构造函数。
在使用设计模式(DesignPattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单。
例如,下面是Factory模式的一个典型实现:
publicstaticCreditgetNewCredit(){
returnnewCredit();
}
改进后的代码使用clone()方法,如下所示:
privatestaticCreditBaseCredit=newCredit();
publicstaticCreditgetNewCredit(){
return(Credit)BaseCredit.clone();
}
上面的思路对于数组处理同样很有用。
2.2.使用非阻塞I/O
版本较低的JDK不支持非阻塞I/OAPI。
为避免I/O阻塞,一些应用采用了创建大量线程的办法(在较好的情况下,会使用一个缓冲池)。
这种技术可以在许多必须支持并发I/O流的应用中见到,如Web服务器、报价和拍卖应用等。
然而,创建Java线程需要相当可观的开销。
JDK1.4引入了非阻塞的I/O库(java.nio)。
如果应用要求使用版本较早的JDK,在这里有一个支持非阻塞I/O的软件包。
请参见Sun中国网站的《调整Java的I/O性能》。
2.3.慎用异常
异常对性能不利。
抛出异常首先要创建一个新的对象。
Throwable接口的构造函数调用名为fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。
只要有异常被抛出,VM就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。
异常只能用于错误处理,不应该用来控制程序流程。
2.4.不要重复初始化变量
默认情况下,调用类的构造函数时,Java会把变量初始化成确定的值:
所有的对象被设置成null,整数变量(byte、short、int、long)设置成0,float和double变量设置成0.0,逻辑值设置成false。
当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键词创建一个对象时,构造函数链中的所有构造函数都会被自动调用。
2.5.尽量使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。
其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。
另外,依赖于具体的编译器/JVM,局部变量还可能得到进一步优化。
请参见《尽可能使用堆栈变量》。
2.6.位移完成乘法和除法
考虑下面的代码:
for(val=0;val<100000;val+=5){alterX=val*8;myResult=val*2;}
用移位操作替代乘法操作可以极大地提高性能。
下面是修改后的代码:
for(val=0;val<100000;val+=5){alterX=val<<3;myResult=val<<1;}
修改后的代码不再做乘以8的操作,而是改用等价的左移3位操作,每左移1位相当于乘以2。
相应地,右移1位操作相当于除以2。
值得一提的是,虽然移位操作速度快,但可能使代码比较难于理解,所以最好加上一些注释。
2.7.避免在循环体中创建对象,即使该对象占用内存空间不大.
for(inti=0;i<10000;++i){
Objectobj=newObject();
System.out.println("obj="+obj);
}
应改成
Objectobj=null;
for(inti=0;i<10000;++i){
obj=newObject();
System.out.println("obj="+obj);
}
2.8.当做数组拷贝操作时,采用System.arraycopy()方法
2.9.尽量避免在循环体中调用方法
2.10.尽量避免在循环体中使用try-catch块
2.11.在多重循环中,如果有可能,尽量将最长的循环放在最内层,最短的循环放在最外层,以减少循环层间的变换次数
2.12.如果预知长度,就设置ArrayList的长度
2.13.避免在类在构造器的初始化其他类
2.14.尽量避免在构造中对静态变量做赋值操作
2.15.不要在类的构造器中创建类的实例
3.JSP/servlet篇
3.1.采用out对象中的print方法代替println()方法
3.2.采用适当的值初始化out对象缓冲区的大小
3.3.尽量采用forward()方法重定向新的JSP
3.4.通过init()方法来缓存一些静态数据以提高应用性能.
3.5.ServletOutputStream取代PrintWriter
4.DB篇
4.1.纯JDBC最快
4.2.最重要的是尽量减少与数据库通信的次数,多使用批处理
4.3.设置合适的Fetch_Size和Batch_Size
4.4.采用连接池技术
4.5.选择合适的事务隔离层与及时关闭连接对象
4.6.PreparedStatement防止sql注入
4.7.尽可能地做批处理更新
5.内存篇
5.1.别用newBoolean()
在很多场景中Boolean类型是必须的,比如JDBC中boolean类型的set与get都是通过Boolean封装传递的,大部分ORM也是用Boolean来封装boolean类型的,比如:
ps.setBoolean("isClosed",newBoolean(true));
ps.setBoolean("isClosed",newBoolean(isClosed));
ps.setBoolean("isClosed",newBoolean(i==3));
通常这些系统中构造的Boolean实例的个数是相当多的,所以系统中充满了大量Boolean实例小对象,这是相当消耗内存的。
Boolean类实际上只要两个实例就够了,一个true的实例,一个false的实例。
Boolean类提供两了个静态变量:
publicstaticfinalBooleanTRUE=newBoolean(true);
publicstaticfinalBooleanFALSE=newBoolean(false);
需要的时候只要取这两个变量就可以了,
比如:
ps.setBoolean("isClosed",Boolean.TRUE);
那么象2、3句那样要根据一个boolean变量来创建一个Boolean怎么办呢?
可以使用Boolean提供的静态方法:
Boolean.valueOf()
比如:
ps.setBoolean("isClosed",Boolean.valueOf(isClosed));
ps.setBoolean("isClosed",Boolean.valueOf(i==3));
因为valueOf的内部实现是:
return(b?
TRUE:
FALSE);
所以可以节省大量内存。
相信如果Java规范直接把Boolean的构造函数规定成private,就再也不会出现这种情况了。
5.2.别用newInteger
和Boolean类似,java开发中使用Integer封装int的场合也非常多,并且通常用int表示的数值通常都非常小。
SUNSDK中对Integer的实例化进行了优化,Integer类缓存了-128到127这256个状态的Integer,如果使用Integer.valueOf(inti),传入的int范围正好在此内,就返回静态实例。
这样如果我们使用Integer.valueOf代替newInteger的话也将大大降低内存的占用。
如果您的系统要在不同的SDK(比如IBMSDK)中使用的话,那么可以自己做了工具类封装一下,比如IntegerUtils.valueOf(),这样就可以在任何SDK中都可以使用这种特性。
5.3.用StringBuffer代替字符串相加
这个我就不多讲了,因为已经被人讲过N次了。
我只想将一个不是笑话的笑话,我在看国内某“著名”java开发的WEB系统的源码中,竟然发现其中大量的使用字符串相加,一个拼装SQL语句的方法中竟然最多构造了将近100个string实例。
无语中!
5.4.过滥使用哈希表
有一定开发经验的开发人员经常会使用hash表(hash表在JDK中的一个实现就是HashMap)来缓存一些数据,从而提高系统的运行速度。
比如使用HashMap缓存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。
其实我们可以使用操作系统中的缓存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。
现在有很多开源的缓存实现项目,比如ehcache、oscache等,这些项目都实现了FIFO、MRU等常见的缓存算法。
5.5.避免过深的类层次结构和过深的方法调用
因为这两者都是非常占用内存的(特别是方法调用更是堆栈空间的消耗大户)。
5.6.变量只有在用到它的时候才定义和实例化
5.7.尽量避免使用static变量,类内私有常量可以用final来代替
6.JVM篇
6.1.内存管理
Java的内存管理就是对象的分配和释放问题。
在Java中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。
对象的释放是由GC决定和执行的。
在Java中,内存的分配是由程序完成的,而内存的释放是由GC完成的,这种收支两条线的方法简化了程序员的工作。
但也加重了JVM的工作。
这也是Java程序运行速度较慢的原因之一。
6.1.1.GC释放空间方法
监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。
当该对象不再被引用时,释放对象。
6.1.2.内存管理结构
Java使用有向图的方式进行内存管理,对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。
将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。
另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。
在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。
如果某个对象(连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
使用有向图方式管理内存的优缺点
Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。
这种方式的优点是管理内存的精度很高,但是效率较低。
另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。
6.1.3.Java的内存泄露
Java虽然由GC来回收内存,但也是存在泄露问题的,只是比C++小一点。
c++所有对象的分配和回收都需要由用户来管理。
即需要管理点,也需要管理边。
若存在不可达的点,无法回收分配给那个点的内存,导致内存泄露。
存在无用的对象引用,自然也会导致内存泄露。
Java由GC来管理内存回收,GC将回收不可达的对象占用的内存空间。
所以,Java需要考虑的内存泄露问题主要是那些被引用但无用的对象——即指要管理边就可以。
被引用但无用的对象,程序引用了该对象,但后续不会再使用它。
它占用的内存空间就浪费了。
如果存在对象的引用,这个对象就被定义为“活动的”,同时不会被释放。
6.1.4.Java内存泄露处理
处理Java的内存泄露问题:
确认该对象不再会被使用。
典型的做法:
把对象数据成员设为null;从集合中移除该对象
注意,当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。
例子:
ListmyList=newArrayList();
for(inti=1;i<100;i++)
{
Objecto=newObject();
myList.add(o);
o=null;
}
//此时,所有的Object对象都没有被释放,因为变量myList引用这些对象。
当myList后来不再用到,将之设为null,释放所有它引用的对象。
之后GC便会回收这些对象占用的内存。
6.2.有关内存管理的经验
6.2.1.尽早释放无用对象的引用
Aa=newA();
//应用a对象
a=null;//当使用对象a之后主动将其设置为空
….
注:
如果a是方法的返回值,不要做这样的处理,否则你从该方法中得到的返回值永远为空,而且这种错误不易被发现、排除
6.2.2.注意集合数据类型,包括数组、树、图、链表等数据结构,
这些数据结构对GC来说,回收更为复杂。
6.2.3.尽量避免在类的默认构造器中创建、初始化大量的对象,
防止在调用其自类的构造器时造成不必要的内存资源浪费
6.2.4.尽量避免强制系统做垃圾内存的回收,增长系统做垃圾回收的最终时间
6.2.5.尽量在合适的场景下使用对象池技术以提高系统性能。