有很多介绍基本的Java应用性能调整的文章他们都讨论些简单的技术诸如使用StringBuffer而不用String使用synchronized关键字的开销等等
这篇文章不再介绍这些东西相反我们关注能帮助你的基于Web的应用更快可升级型更好的技巧一些技巧很详细其他的相对简短但所有的都很有用最后以一些你可提供给你的管理者的建议结束
我写这篇文章的灵感来自于当我的同事和我一起回忆我们(dotcom)时代的时候——我们如何设计能支持成千上万的用户和拥有紧密代码的系统我们如何对有侵略性的致命打击有时在为复用设计和为性能设计之间有一个权衡基于我的情况性能每次都获胜即使你的商务顾客无需理解代码复用但是他们知道快速(fastperforming)的系统是怎么回事让我们开始看看我们的技巧
如何使用Exception
Exception降低性能一个异常抛出首先需要创建一个新的对象Throwable接口中的构造器调用名为fillInStackTrace()的本地方法这个方法负责巡检栈的整个框架来收集跟蹤信息这样无论何时有异常抛出它要求虚拟机装载调用栈因为一个新的对象在中部被创建
异常应当仅用于有错误发生时而不要控制流
我有机会在一个专门用于无线内容市场的网站(名字故意隐去了)看到一段代码其中开发者完全可以使用一个简单的对照来查看对象是否为空相反他或她跳过了这个检查而实际上抛出NullPointerException
不要两次初始化变量
Java通过调用独特的类构造器默认地初始化变量为一个已知的值所有的对象被设置成nullintegers (byte short int long)被设置成float和double设置成Boolean变量设置成false这对那些扩展自其它类的类尤其重要这跟使用一个新的关键词创建一个对象时所有一连串的构造器被自动调用一样
对新的关键词使用优选法则
正如前面提到的通过使用一个新的关键词创建一个类的实例在这个链中的所有构造器将被调用如果你需要创建一个类的新实例你可以使用一个实现了cloneable接口的对象的clone()方法该clone方法不调用任何类的构造器
如果你已经使用了设计模式作为你的体系结构的一部分并且使用了工厂模式创建对象变化会很简单下面所列是工厂模式的典型实现
public static Account getNewAccount() {
return new Account();
}
使用了clone方法的refactored代码看起来可能像下面这样
private static Account BaseAccount = new Account();
public static Account getNewAccount() {
return (Account) BaseAccountclone();
}
以上的思路对实现数组同样有用
如果你在应用中没有使用设计模式我建议你停止读这篇文章赶快跑到(不要走)书店挑一本四人着的《设计模式》
在任何可能的地方让类为Final
标记为final的类不能被扩展在《核心Java API》中有大量这个技术的例子诸如javalangString将String类标记为final阻止了开发者创建他们自己实现的长度方法
更深入点说如果类是final的所有类的方法也是final的Java编译器可能会内联所有的方法(这依赖于编译器的实现)在我的测试里我已经看到性能平均增加了%
在任何可能的地方使用局部变量
属于方法调用部分的自变量和声明为此调用一部分的临时变量存储在栈中这比较快诸如static实例(instance)变量和新的对象创建在堆中这比较慢局部变量的更深入优化依赖于你正在使用的编译器或虚拟机
使用NonblockingI/O
当前的JDK版本不支持nonblocking I/O API很多应用试图通过创建大量的线程(目光长远得用在池中)来避免阻塞正如前述在Java中创建线程有严重的开销
典型的你可能看到应用中实现的线程需要支持并发I/O流像Web 服务器并quote and auction components
JDK介绍了一个nonblocking I/O包(javanio)如果你必须保留在较早版本的JDK有添加了支持nonblocking I/O的第三方包
wwwcsberkeleyedu/~mdw/proj/javanbio/l
停止小聪明
很多开发人员在脑子中编写可复用和灵活的代码而有时候在他们的程序中就产生额外的开销曾经或者另外的时候他们编写了类似这样的代码
public void doSomething(File file) {
FileInputStream fileIn = new FileInputStream(file);
// do something
他够灵活但是同时他们也产生了更多的开销这个主意背后做的事情是操纵一个InputStream而不是一个文件因此它应该重写如下
public void doSomething(InputStream inputStream){
// do something
乘法和除法
我有太多的东东适用于摩尔法则——它声明CPU功率每年成倍增长摩尔法则表明每年由开发者所写的差劲的代码数量三倍增加划去了摩尔法则的任何好处
考虑下面的代码
for (val = ; val < 100000; val +=5) { shiftX = val * 8; myRaise = val * 2; }
如果我们狡猾的利用位移(bit),性能将会六倍增加。这是重写的代码:
for (val = 0; val < 100000; val += 5) { shiftX = val << 3; myRaise = val << 1; }
代替了乘以8,我们使用同等效果的左移3位。每一个移动相当于乘以2,变量myRaise对此做了证明。同样向右移位相当于除以2,当然这会使执行速度加快,但可能会使你的东东以后难于理解;所以这只是个建议。
选择一个基于垃圾收集实现的虚拟机
许多人可能会对Java规范不需要实现垃圾收集感到惊讶。设想时代已经是我们都拥有无限内存计算机。总之,垃圾收集器日常事务就是负责发现和抛出(hence garbage)不再需要的对象。垃圾收集必须发现那些对象不再被程序指向,并且使被对象占用的栈内存被释放掉。它还负责运行任何被释放对象的finalizer。
垃圾收集故意不允许你释放并非由你分配的内存,从而帮助你确保程序完整,当JVM确定CPU时间的时间表并且当垃圾收集器运行时,这个进程也产生开销。
垃圾收集器有两个不同的步骤执行他们的工作。
实现了定位计算的垃圾收集器在栈中为每一个对象保留一个计数。当一个对象被创建并且对它的一个定位被分配给一个变量,计数增加。当对象越出范围,定位计数被设置成0并且对象可以被垃圾收集。这个步骤允许参考计数器运行在与程序执行有关的短时间增量内。定位计数在父子彼此拥有定位的应用里运行不正常。每次一个对象刷新时也会有定位计数增加和减少的开销。
实现了跟蹤的垃圾收集器从根节点开始跟蹤一列定位。对象发现跟蹤是否被标记。在这个过程完成后,知道不可达的任何没标记的对象可以被垃圾收集。这可能以位图(bitmap)形式实现或者在对象中被设置标志。此技术参考"Mark and Sweep."(reference:定位,翻译成“指向”好像更容易理解,是Java语言对在用对象的一个跟蹤指针。译者着)
给你的管理人员提建议
其他方法可被用来使你的基于Web的应用更快并且更可升级。可实现的最简单的技术通常是支持cluster的策略。使用cluster,一组服务器能够一起透明的提供服务。多数应用服务器允许你获得cluster支持而不需要改变你的应用——一个大的胜利。
当然在执行此步骤之前你可能需要考虑来自你使用的应用服务器提供商附加的许可权利。
当看到cluster策略会有许多额外的事情考虑。经常在体系结构中产生的一个缺点是拥有有状态会话。如果cluster中的一个服务器或者进程当掉,cluster会捨弃整个应用。为防止此类事情发生,cluster必须给cluster中的所有成员不断复制会话Bean的状态。确保你也限制了存储在会话中的对象的大小和数量,因为这些也需要被复制。
Cluster也允许你分期度量你的Web站点的部分。如果你需要度量静态部分,你可以添加Web服务器。如果你需要度量动态生成的部分,你可以添加应用服务器。
在你已经把你的系统放入cluster后,下一个让你的应用跑得更快的建议步骤是选择一个更好的虚拟机。看看Hotspot虚拟机或者其他的飞速发展中的执行优化的虚拟机。随同虚拟机,看看更好的编译器是一个更好的主意。
如果你使用了几个这儿提到的行业技术插件,并且仍然不能获得你要的可升级性和高可用性,那么我建议一个可靠的调试策略。策略的第一步是为可能的瓶颈检查整个体系结构。通常,这在你的作为单线程组件或者有很多辅助连接线组件的UML流图中很容易识别出来。
最后的步骤是产生一个整个代码的详细性能估价。
确保你的管理人员至少为此安排了整个项目时间的20%;否则不足的时间可能不止危及你整个成功的安全,还会导致你向系统引入新的缺点。
许多组织者在适当的位置没有严格意义的测试基础而归咎于成本考虑也是错误的。确保你的QA环境真实反映你的生产环境,并且你的QA测试考虑以不同的负载测试应用,包括在最大的预期并发用户时一个基于低负载和一个完全负载的测试。
性能测试,有时测试一个系统的稳定性,可能需要在每天,甚至每周的整个时期的不同关节都运行。