概述
随着Java的广泛应用,越来越多的关键企业系统也使用Java构建。作为Java核心运行环境的Java虚拟机JVM被广泛地部署在各种系统平台上。对Java应用的性能优化也越来越受到关注;谈到Java应用的性能问题就不得不涉及到两个方面:一是Java应用的构造是否是最优化的;二是对JVM的微调。本文将从一般意义上对Java性能的优化做一些总结。
Java性能优化的策略
一谈到性能优化,往往会被认为是应用开发和部署过程中或之后的事情,其实不然。如果想要构建一个最优化的系统,我们必须从该系统的需求��析和业务模型设计之初就要考虑到性能的最优化问题;当然对于一个已经构造好的系统来讲,我们能做的只是在不改变系统代码的前提下,尽量地在该系统的部署方案和运行环境上下功夫。由此,我们得出一个结论就是:所谓最优化是一个相对的概念,一个系统是否是最优化的,必须基于某个大前提来进行评判。因此,在进行优化分析之前一定要把握好前提条件是什么。
如上图所示,可以看出,对系统性能提高贡献最大、最明显的是从业务层面和架构层面所作的分析和优化;最不明显的是对系统平台和硬件层面以及网络层面的优化。因此在着手对目标系统进行优化分析之前,我们一定要从优化最明显、贡献最大的方面着手。这样有助于我们在最大程度上去提高系统性能。
以下我们将针对Java系统的性能优化,从代码编写和JVM两个角度着手,总结一下常见的方法和思路。
编写性能高效的Java代码
根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议:
1)避免对象创建和GC
只要有可能,应该避免创建对象,防止调用构造函数带来的相关性能成本,以及在对象结束其生命周期时进行垃圾收集所带来的成本。考虑以下这些准则:
只要有可能,就使用基本变量类型,而不使用对象类型。例如,使用 int,而不使用 Integer;
缓存那些频繁使用的寿命短的对象,避免一遍又一遍地重复创建相同的对象,并因此加重垃圾收集的负担;
在处理字符串时,使用 StringBuffer 而不使用字符串String进行连接操作,因为字符串对象具有不可变的特性,并且需要创建额外的字符串对象以完成相应的操作,而这些对象最终必须经历 GC;
避免过度地进行 Java 控制台的写操作,降低字符串对象处理、文本格式化和输出带来的成本;
实现数据库连接池,重用连接对象,而不是重复地打开和关闭连接;
使用线程池(thread pooling),避免不停地创建和删除线程对象,特别是在大量使用线程的时候;
避免在代码中调用GC。GC是一个“停止所有处理(stop the world)”的事件,它意味着除了 GC 线程自身外,其他所有执行线程都将处于挂起状态。如果必须调用 GC,那么可以在非紧急阶段或空闲阶段实现它;
避免在循环内分配对象。
尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在使用这种方式时候,必须特别注意一些复杂的对象,例如数组,队列,树,图等,这些对象之间的相互引用关系较为复杂。对于这类对象,GC回收它们一般效率较低。如果程序允许,尽早将不再使用的引用对象赋为null。这样可以加速GC的工作。
如果有经常使用的图片,可以使用soft引用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起Out Of Memory。
注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。
2)Java Native Interface(JNI)
使用本机代码编写应用程序的一部分,特别是频繁使用的部分,并将之与 Java 链接,这样做通常是为了提高性能。不过,JVM 与本机代码之间的通信通常很慢,因此,太多的 JNI 调用可能会降低性能。只要有可能就应该将本机操作集合在一起,以减少 JNI 调用的数量。使用 JNI 代码本地处理异常,尽管有时不可避免,但会导致性能下降。在这种情况下,应该使用 ExceptionCheck() 函数,因为与 ExceptionOccurred() 相比较,它带来的计算开销更少一些。后者必须创建一个将引用的对象,以及一个本地引用。
3)同步
为了减少 JVM 和操作系统中的争用,应该只在可行的情况下才使用同步方法。不要将同步方法放到循环结构中。
4)数据结构
作为一条通用规则,在更简单的数据结构能满足需要的地方,应该避免使用更复杂的数据结构。例如,在可以使用数组的地方不要使用向量。使用最有效的方法搜索元素,并将元素插入数据结构中,比如说,在向量的结尾处添加和删除元素,以便获得更好的性能。
5)尽可能使用堆栈变量
如果您频繁存取变量,就需要考虑从何处存取这些变量。变量是static变量,还是堆栈变量,或者是类的实例变量? 变量的存储位置对存取它的代码的性能有明显的影响。JVM 是一种基于堆栈的虚拟机,因此优化了对堆栈数据的存取和处理。所有局部变量都存储在一个局部变量表中,在 Java 操作数堆栈中进行处理,并可被高效地存取。存取 static 变量和实例变量成本更高,因为 JVM 必须使用代价更高的操作码,并从常数存储池中存取它们。(常数存储池保存一个类型所使用的所有类型、字段和方法的符号引用。)通常,在第一次从常数存储池中访问 static 变量或实例变量以后,JVM 将动态更改字节码以使用效率更高的操作码。尽管有这种优化,堆栈变量的存取仍然更快。
考虑到这些事实,在构建代码时就可以考虑通过存取堆栈变量而不是实例变量或 static 变量,使操作更高效。如果必须使用,可以考虑将实例变量或 static 变量复制到局部堆栈变量中。当变量的处理完成以后,其值又被复制回实例变量或 static 变量中。这并不表示您应该避免使用 static 变量或实例变量。您应该使用对您的设计有意义的存储机制。例如,如果您在一个循环中存取 static 变量或实例变量,则您可以临时将它们存储在一个局部堆栈变量中,这样就可以明显地提高代码的性能。这将提供最高效的字节码指令序列供 JVM 执行。
考虑到这些事实,在构建代码时就可以考虑通过存取堆栈变量而不是实例变量或 static 变量,使操作更高效。如果必须使用,可以考虑将实例变量或 static 变量复制到局部堆栈变量中。当变量的处理完成以后,其值又被复制回实例变量或 static 变量中。这并不表示您应该避免使用 static 变量或实例变量。您应该使用对您的设计有意义的存储机制。例如,如果您在一个循环中存取 static 变量或实例变量,则您可以临时将它们存储在一个局部堆栈变量中,这样就可以明显地提高��码的性能。这将提供最高效的字节码指令序列供 JVM 执行。
6)finalize函数
finalize是位于Object类的一个方法,该方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易访问到这个方法。由于,finalize函数没有自动实现链式调用,我们必须手动的实现,因此finalize函数的最后一个语句通常是 super.finalize()。通过这种方式,我们可以从下到上实现finalize的调用,即先释放自己的资源,然后再释放父类的资源。
根据Java语言规范,JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。另外,规范还保证finalize函数最多运行一次。
很多Java初学者会认为这个方法类似于C++中的析构函数,将很多对象、资源的释放都放在这一函数里面。其实,这不是一种很好的方式。原因有三,其一,GC为了能够支持finalize函数,要对覆盖这个函数的对象作很多附加的工作。其二,在finalize运行完成之后,该对象可能变成可达的,GC还要再检查一次该对象是否是可达的。因此,使用finalize会降低GC的运行性能。其三,由于GC调用finalize的时间是不确定的,因此通过这种方式释放资源也是不确定的。
通常,finalize用于一些不容易控制、并且非常重要的资源的释放,例如一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。在这种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源���式为辅,形成一种双保险的管理机制,而不应该仅仅依靠finalize来释放资源。
7)异常的开销很大
是的,异常开销很大。那么,这是不是就意味着您不该使用异常?当然不是。但是,何时应该使用异常,何时又不应该使用异常呢?不幸的是,答案不是一下子就说得清的。我们要说的是,您不必放弃已经学到的好的 try-catch 编程习惯,但是使用异常时可能会遇到麻烦,创建异常就是一个例子。当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。运行时栈不是为有效的异常创建而设计的,而是设计用来让运行时尽可能快且没有任何不必要的延迟。但是,当需要创建一个 Exception 时,JVM 不得不说:“先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。”栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素,从栈顶到栈底,还有行号和一切应有的东西。如果在一个深度为20的栈中创建了异常,那么就别指望只记录顶部的几个栈元素了——得完完整整地记录下所有 20个元素。从 main 或 Thread.run (在栈底)到栈顶,记录整个栈。
因此,创建异常这一部分开销很大。从技术上讲,栈跟踪快照是在本地方法 Throwable.fillInStackTrace() 中发生的,这个方法又是从 Throwable constructor 那里调用的。但是这并没有什么影响——如果您创建一个 Exception ,就得付出代价。好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,您甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常。幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。
8)避免非常大的分配
有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配的内存块都必须是连续的,而随着堆越来越满,找到较大的连续块越来越困难。这不仅仅是 Java 的问题,使用 C 中的 malloc 也会遇到这个问题。JVM 在压缩阶段通过重新分配引用来减少碎片,但其代价是要冻结应用程序较长的时间。
该贴由koei转至本版2014-5-2 16:06:48