1、Java 性能的优化Java 在九十年代中期出现以后,在赢得赞叹的同时,也引来了一些批评。赢得的赞叹主要是 Java 的跨平台的操作性,即所谓的”Write Once,Run Anywhere”.但由于 Java 的性能和运行效率同 C 相比,仍然有很大的差距,从而引来了很多的批评。对于服务器端的应用程序,由于不大涉及到界面设计和程序的频繁重启,Java 的性能问题看似不大明显,从而一些 Java 的技术,如 JSP,Servlet,EJB 等在服务器端编程方面得到了很大的应用,但实际上,Java 的性能问题在服务器端依然存在。下面我将分四个方面来讨论 Java 的性能和执行效率以及提高 Ja
2、va性能的一些方法。一关于性能的基本知识1性能的定义在我们讨论怎样提高 Java 的性能之前,我们需要明白“性能“的真正含义。我们一般定义如下五个方面作为评判性能的标准。1) 运算的性能-哪一个算法的执行性能最好2) 内存的分配-程序需要分配多少内存,运行时的效率和性能最高。3) 启动的时间-程序启动需要多少时间。4) 程序的可伸缩性-程序在用户负载过重的情况下的表现。5) 性能的认识-用户怎样才能认识到程序的性能。对于不同的应用程序,对性能的要求也不同。例如,大部分的应用程序在启动时需要较长的时间,从而对启动时间的要求有所降低;服务器端的应用程序通常都分配有较大的内存空间,所以对内存的要求也
3、有所降低。但是,这并不是所这两方面的性能可以被忽略。其次,算法的性能对于那些把商务逻辑运用到事务性操作的应用程序来讲非常重要。总的来讲,对应用程序的要求将决定对各个性能的优先级。2怎样才能提高 JAVA 的性能提高 JAVA 的性能,一般考虑如下的四个主要方面:(1) 程序设计的方法和模式一个良好的设计能提高程序的性能,这一点不仅适用于 JAVA,也适用也任何的编程语言。因为它充分利用了各种资源,如内存,CPU,高速缓存,对象缓冲池及多线程,从而设计出高性能和可伸缩性强的系统。当然,为了提高程序的性能而改变原来的设计是比较困难的,但是,程序性能的重要性常常要高于设计上带来的变化。因此,在编程开
4、始之前就应该有一个好的设计模型和方法。(2) JAVA 布署的环境。JAVA 布署的环境就是指用来解释和执行 JAVA 字节码的技术,一般有如下五种。即解释指令技术(Interpreter Technology),及时编译的技术(Just In Time Compilier Technology), 适应性优化技术(Adaptive Optimization Technology), 动态优化,提前编译为机器码的技术(Dynamic Optimization,Ahead Of Time Technology)和编译为机器码的技术(Translator Technology).这些技术一般都通过
5、优化线程模型,调整堆和栈的大小来优化 JAVA 的性能。在考虑提高 JAVA 的性能时,首先要找到影响 JAVA 性能的瓶颈(BottleNecks),在确认了设计的合理性后,应该调整 JAVA 布署的环境,通过改变一些参数来提高 JAVA 应用程序的性能。具体内容见第二节。(3) JAVA 应用程序的实现当讨论应用程序的性能问题时,大多数的程序员都会考虑程序的代码,这当然是对的,当更重要的是要找到影响程序性能的瓶颈代码。为了找到这些瓶颈代码,我们一般会使用一些辅助的工具,如Jprobe,Optimizit,Vtune 以及一些分析的工具如 TowerJ Performance 等。这些辅助的
6、工具能跟踪应用程序中执行每个函数或方法所消耗掉的时间,从而改善程序的性能。(4) 硬件和操作系统为了提高 JAVA 应用程序的性能,而采用跟快的 CPU 和更多的内存,并认为这是提高程序性能的唯一方法,但事实并非如此。实践经验和事实证明,只有遭到了应用程序性能的瓶颈,从而采取适当得方法,如设计模式,布署的环境,操作系统的调整,才是最有效的。3程序中通常的性能瓶颈。所有的应用程序都存在性能瓶颈,为了提高应用程序的性能,就要尽可能的减少程序的瓶颈。以下是在 JAVA 程序中经常存在的性能瓶颈。了解了这些瓶颈后,就可以有针对性的减少这些瓶颈,从而提高 JAVA 应用程序的性能4. 提高 JAVA 程
7、序性能的步骤为了提高 JAVA 程序的性能,需要遵循如下的六个步骤。a) 明确对性能的具体要求在实施一个项目之前,必须要明确该项目对于程序性能的具体要求,如:这个应用程序要支持 5000个并发的用户,并且响应时间要在 5 秒钟之内。但同时也要明白对于性能的要求不应该同对程序的其他要求冲突。b) 了解当前程序的性能你应该了解你的应用程序的性能同项目所要求性能之间的差距。通常的指标是单位时间内的处理数和响应时间,有时还会比较 CPU 和内存的利用率。c) 找到程序的性能瓶颈为了发现程序中的性能瓶颈,通常会使用一些分析工具,如:TowerJ Application Performance Analy
8、zer 或 VTune 来察看和分析程序堆栈中各个元素的消耗时间,从而正确的找到并改正引起性能降低的瓶颈代码,从而提高程序的性能。这些工具还能发现诸如过多的异常处理,垃圾回收等潜在的问题。d) 采取适当的措施来提高性能找到了引起程序性能降低的瓶颈代码后,我们就可以用前面介绍过的提高性能的四个方面,即设计模式,JAVA 代码的实现,布署 JAVA 的环境和操作系统来提高应用程序的性能。具体内容将在下面的内容中作详细说明。e) 只进行某一方面的修改来提高性能一次只改变可能引起性能降低的某一方面,然后观察程序的性能是否有所提高,而不应该一次改变多个方面,因为这样你将不知道到底哪个方面的改变提高了程序
9、的性能,哪个方面没有,即不能知道程序瓶颈在哪。f) 返回到步骤 c,继续作类似的工作,一直达到要求的性能为止。二 JAVA 布署的环境和编译技术开发 JAVA 应用程序时,首先把 JAVA 的源程序编译为与平台无关的字节码。这些字节码就可以被各种基于 JVM 的技术所执行。这些技术主要分为两个大类。即基于解释的技术和基于提前编译为本地码的技术。其示意图如下:具体可分为如下的五类: a) 解释指令技术其结构图和执行过程如下:JAVA 的编译器首先把 JAVA 源文件编译为字节码。这些字节码对于 JAVA 虚拟机(JVM)来讲就是机器的指令码。然后,JAVA 的解释器不断的循环取出字节码进行解释并
10、执行。这样做的优点是可以实现 JAVA 语言的跨平台,同时生成的字节码也比较紧凑。JAVA 的一些优点,如安全性,动态性都得保持;但缺点是省生成的字节码没有经过什么优化,同全部编译好的本地码相比,速度比较慢。b) 及时编译技术(Just In Time)及时编译技术是为了解决指令解释技术效率比较低,速度比较慢的情况下提出的,其结构图如下所示。其主要变化是在 JAVA 程序执行之前,又 JIT 编译器把 JAVA 的字节码编译为机器码。从而在程序运行时直接执行机器码,而不用对字节码进行解释。同时对代码也进行了部分的优化。这样做的优点是大大提高了 JAVA 程序的性能。同时,由于编译的结果并不在程
11、序运行间保存,因此也节约了存储空间了加载程序的时间;缺点是由于 JIT 编译器对所有的代码都想优化,因此也浪费了很多的时间。IBM 和 SUN 公司都提供了相关的 JIT 产品。c) 适应性优化技术(Adaptive Optimization Technology)同 JIT 技术相比,适应性优化技术并不对所有的字节码进行优化。它会跟踪程序运行的成个过程,从而发现需要优化的代码,对代码进行动态的优化。对优化的代码,采取 80/20 的策略。从理论上讲,程序运行的时间越长,代码就越优化。其结构图如下:其优点是适应性优化技术充分利用了程序执行时的信息,发行程序的性能瓶颈,从而提高程序的性能;其缺点
12、是在进行优化时可能会选择不当,发而降低了程序的性能。其主要产品又 IBM,SUN 的 HotSpot.d) 动态优化,提前编译为机器码的技术(Dynamic Optimization,Ahead Of Time)动态优化技术充分利用了 JAVA 源码编译,字节码编译,动态编译和静态编译的技术。其输入时 JAVA的原码或字节码,而输出是经过高度优化的可执行代码和个来动态库的混合(Window 中是 DLL 文件,UNIX中是共享库.a .so 文件)。其结构如下:其优点是能大大提高程序的性能;缺点是破坏了 JAVA 的可移植性,也对 JAVA 的安全带来了一定的隐患。三优化 JAVA 程序设计和
13、编码,提高 JAVA 程序性能的一些方法。通过使用一些前面介绍过的辅助性工具来找到程序中的瓶颈,然后就可以对瓶颈部分的代码进行优化。一般有两种方案:即优化代码或更改设计方法。我们一般会选择后者,因为不去调用以下代码要比调用一些优化的代码更能提高程序的性能。而一个设计良好的程序能够精简代码,从而提高性能。下面将提供一些在 JAVA 程序的设计和编码中,为了能够提高 JAVA 程序的性能,而经常采用的一些方法和技巧。1对象的生成和大小的调整。JAVA 程序设计中一个普遍的问题就是没有好好的利用 JAVA 语言本身提供的函数,从而常常会生成大量的对象(或实例)。由于系统不仅要花时间生成对象,以后可能
14、还需花时间对这些对象进行垃圾回收和处理。因此,生成过多的对象将会给程序的性能带来很大的影响。例 1:关于 String ,StringBuffer,+和 appendJAVA 语言提供了对于 String 类型变量的操作。但如果使用不当,会给程序的性能带来影响。如下面的语句:String name=new String(“HuangWeiFeng”);System.out.println(name+”is my name”);看似已经很精简了,其实并非如此。为了生成二进制的代码,要进行如下的步骤和操作。(1) 生成新的字符串 new String(STR_1);(2) 复制该字符串。(3) 加
15、载字符串常量”HuangWeiFeng”(STR_2);(4) 调用字符串的构架器(Constructor);(5) 保存该字符串到数组中(从位置 0 开始)(6) 从 java.io.PrintStream 类中得到静态的 out 变量(7) 生成新的字符串缓冲变量 new StringBuffer(STR_BUF_1);(8) 复制该字符串缓冲变量(9) 调用字符串缓冲的构架器(Constructor);(10) 保存该字符串缓冲到数组中(从位置 1 开始)(11) 以 STR_1 为参数,调用字符串缓冲(StringBuffer)类中的 append 方法。(12) 加载字符串常量”is
16、 my name”(STR_3);(13) 以 STR_3 为参数,调用字符串缓冲(StringBuffer)类中的 append 方法。(14) 对于 STR_BUF_1 执行 toString 命令。(15) 调用 out 变量中的 println 方法,输出结果。由此可以看出,这两行简单的代码,就生成了 STR_1,STR_2,STR_3,STR_4 和 STR_BUF_1 五个对象变量。这些生成的类的实例一般都存放在堆中。堆要对所有类的超类,类的实例进行初始化,同时还要调用类极其每个超类的构架器。而这些操作都是非常消耗系统资源的。因此,对对象的生成进行限制,是完全有必要的。经修改,上面
17、的代码可以用如下的代码来替换。StringBuffer name=new StringBuffer(“HuangWeiFeng”);System.out.println(name.append(“is my name.”).toString();系统将进行如下的操作。(1) 生成新的字符串缓冲变量 new StringBuffer(STR_BUF_1);(2) 复制该字符串缓冲变量(3) 加载字符串常量”HuangWeiFeng”(STR_1);(4) 调用字符串缓冲的构架器(Constructor);(5) 保存该字符串缓冲到数组中(从位置 1 开始)(6) 从 java.io.PrintS
18、tream 类中得到静态的 out 变量(7) 加载 STR_BUF_1;(8) 加载字符串常量”is my name”(STR_2);(9) 以 STR_2 为参数,调用字符串缓冲(StringBuffer)实例中的 append 方法。(10) 对于 STR_BUF_1 执行 toString 命令。(STR_3)(11)调用 out 变量中的 println 方法,输出结果。由此可以看出,经过改进后的代码只生成了四个对象变量:STR_1,STR_2,STR_3 和 STR_BUF_1.你可能觉得少生成一个对象不会对程序的性能有很大的提高。但下面的代码段 2 的执行速度将是代码段 1 的
19、2 倍。因为代码段 1 生成了八个对象,而代码段 2 只生成了四个对象。代码段 1:String name= new StringBuffer(“HuangWeiFeng”);name+=”is my”;name+=”name”;代码段 2:StringBuffer name=new StringBuffer(“HuangWeiFeng”);name.append(“is my”);name.append(“name.”).toString();因此,充分的利用 JAVA 提供的库函数来优化程序,对提高 JAVA 程序的性能时非常重要的.其注意点主要有如下几方面;(1) 尽可能的使用静态变量(
20、Static Class Variables)如果类中的变量不会随他的实例而变化,就可以定义为静态变量,从而使他所有的实例都共享这个变量。例:public class fooSomeObject so=new SomeObject();就可以定义为:public class foostatic SomeObject so=new SomeObject();(2) 不要对已生成的对象作过多的改变。对于一些类(如:String 类)来讲,宁愿在重新生成一个新的对象实例,而不应该修改已经生成的对象实例。例:String name=”Huang”;name=”Wei”;name=”Feng”;上述代码
21、生成了三个 String 类型的对象实例。而前两个马上就需要系统进行垃圾回收处理。如果要对字符串进行连接的操作,性能将得更差。因为系统将不得为此生成更多得临时变量。如上例 1 所示。(3) 生成对象时,要分配给它合理的空间和大小JAVA 中的很多类都有它的默认的空间分配大小。对于 StringBuffer 类来讲,默认的分配空间大小是16 个字符。如果在程序中使用 StringBuffer 的空间大小不是 16 个字符,那么就必须进行正确的初始化。(4) 避免生成不太使用或生命周期短的对象或变量。对于这种情况,因该定义一个对象缓冲池。以为管理一个对象缓冲池的开销要比频繁的生成和回收对象的开销小
22、的多。(5) 只在对象作用范围内进行初始化。JAVA 允许在代码的任何地方定义和初始化对象。这样,就可以只在对象作用的范围内进行初始化。从而节约系统的开销。例:SomeObject so=new SomeObject();If(x=1) thenFoo=so.getXX();可以修改为:if(x=1) thenSomeObject so=new SomeObject();Foo=so.getXX();2异常(Exceptions)JAVA 语言中提供了 try/catch 来发方便用户捕捉异常,进行异常的处理。但是如果使用不当,也会给 JAVA 程序的性能带来影响。因此,要注意以下两点。(1)
23、 避免对应用程序的逻辑使用 try/catch如果可以用 if,while 等逻辑语句来处理,那么就尽可能的不用 try/catch 语句(2) 重用异常在必须要进行异常的处理时,要尽可能的重用已经存在的异常对象。以为在异常的处理中,生成一个异常对象要消耗掉大部分的时间。3. 线程(Threading)一个高性能的应用程序中一般都会用到线程。因为线程能充分利用系统的资源。在其他线程因为等待硬盘或网络读写而 时,程序能继续处理和运行。但是对线程运用不当,也会影响程序的性能。例 2:正确使用 Vector 类Vector 主要用来保存各种类型的对象(包括相同类型和不同类型的对象)。但是在一些情况下
24、使用会给程序带来性能上的影响。这主要是由 Vector 类的两个特点所决定的。第一,Vector 提供了线程的安全保护功能。即使 Vector 类中的许多方法同步。但是如果你已经确认你的应用程序是单线程,这些方法的同步就完全不必要了。第二,在 Vector 查找存储的各种对象时,常常要花很多的时间进行类型的匹配。而当这些对象都是同一类型时,这些匹配就完全不必要了。因此,有必要设计一个单线程的,保存特定类型对象的类或集合来替代 Vector 类.用来替换的程序如下(StringVector.java):public class StringVectorprivate String data;pr
25、ivate int count;public StringVector() this(10); / default size is 10 public StringVector(int initialSize)data = new StringinitialSize;public void add(String str)/ ignore null stringsif(str = null) return; ensureCapacity(count + 1);datacount+ = str;private void ensureCapacity(int minCapacity)int oldC
26、apacity = data.length;if (minCapacity oldCapacity)String oldData = data;int newCapacity = oldCapacity * 2;data = new StringnewCapacity;System.arraycopy(oldData, 0, data, 0, count);public void remove(String str)if(str = null) return / ignore null str for(int i = 0; i count)return null; / index is # s
27、tringselse return dataindex; / index is good /* * * * * * * * * * * * * * * *StringVector.java * * * * * * * * * * * * * * * * */因此,代码:Vector Strings=new Vector();Strings.add(“One”);Strings.add(“Two”);String Second=(String)Strings.elementAt(1);可以用如下的代码替换:StringVector Strings=new StringVector();Strin
28、gs.add(“One”);Strings.add(“Two”);String Second=Strings.getStringAt(1);这样就可以通过优化线程来提高 JAVA 程序的性能。用于测试的程序如下(TestCollection.java):import java.util.Vector;public class TestCollectionpublic static void main(String args )TestCollection collect = new TestCollection();if(args.length = 0)System.out.println(“
29、Usage: java TestCollection vector | stringvector “);System.exit(1);if(args0.equals(“vector“)Vector store = new Vector();long start = System.currentTimeMillis();for(int i = 0; i 1000000; i+)store.addElement(“string“);long finish = System.currentTimeMillis();System.out.println(finish-start);start = Sy
30、stem.currentTimeMillis();for(int i = 0; i 1000000; i+)String result = (String)store.elementAt(i);finish = System.currentTimeMillis();System.out.println(finish-start);else if(args0.equals(“stringvector“)StringVector store = new StringVector();long start = System.currentTimeMillis();for(int i = 0; i 1
31、000000; i+) store.add(“string“); long finish = System.currentTimeMillis();System.out.println(finish-start);start = System.currentTimeMillis();for(int i = 0; i 1000000; i+) String result = store.getStringAt(i);finish = System.currentTimeMillis();System.out.println(finish-start);/* * * * * * * * * * *
32、 * * * * *TestCollection.java * * * * * * * * * * * * * * * * */测试的结果如下(假设标准的时间为,越小性能越好):关于线程的操作,要注意如下几个方面。(1) 防止过多的同步如上所示,不必要的同步常常会造成程序性能的下降。因此,如果程序是单线程,则一定不要使用同步。(2) 同步方法而不要同步整个代码段对某个方法或函数进行同步比对整个代码段进行同步的性能要好。(3) 对每个对象使用多”锁”的机制来增大并发。一般每个对象都只有一个”锁”,这就表明如果两个线程执行一个对象的两个不同的同步方法时,会发生”死锁”。即使这两个方法并不共享任何资
33、源。为了避免这个问题,可以对一个对象实行”多锁”的机制。如下所示:class fooprivate static int var1;private static Object lock1=new Object();private static int var2;private static Object lock2=new Object();public static void increment1()synchronized(lock1)var1+;public static void increment2()synchronized(lock2)var2+;4输入和输出(I/O)输入和输出
34、包括很多方面,但涉及最多的是对硬盘,网络或数据库的读写操作。对于读写操作,又分为有缓存和没有缓存的;对于数据库的操作,又可以有多种类型的 JDBC 驱动器可以选择。但无论怎样,都会给程序的性能带来影响。因此,需要注意如下几点:(1) 使用输入输出缓冲尽可能的多使用缓存。但如果要经常对缓存进行刷新(flush),则建议不要使用缓存。(2) 输出流(Output Stream)和 Unicode 字符串当时用 Output Stream 和 Unicode 字符串时,Write 类的开销比较大。因为它要实现 Unicode 到字节(byte)的转换.因此,如果可能的话,在使用 Write 类之前就
35、实现转换或用 OutputStream 类代替 Writer类来使用。(3) 当需序列化时使用 transient当序列化一个类或对象时,对于那些原子类型(atomic)或可以重建的原素要表识为 transient 类型。这样就不用每一次都进行序列化。如果这些序列化的对象要在网络上传输,这一小小的改变对性能会有很大的提高。 (4) 使用高速缓存(Cache)对于那些经常要使用而又不大变化的对象或数据,可以把它存储在高速缓存中。这样就可以提高访问的速度。这一点对于从数据库中返回的结果集尤其重要。(5) 使用速度快的 JDBC 驱动器(Driver)JAVA 对访问数据库提供了四种方法。这其中有两种是 JDBC 驱动器。一种是用 JAVA 外包的本地驱动器;另一种是完全的 JAVA 驱动器。具体要使用哪一种得根据 JAVA 布署的环境和应用程序本身来定。5.一些其他的经验和技巧(1) 使用局部变量(2) 避免在同一个类中动过调用函数或方法(get 或 set)来设置或调用变量。(3) 避免在循环中生成同一个变量或调用同一个函数(参数变量也一样)(4) 尽可能的使用 static,final,private 等关键字(5) 当复制大量数据时,使用 System.arraycopy()命令。