java

位置:IT落伍者 >> java >> 浏览文章

Java性能优化


发布日期:2018年05月18日
 
Java性能优化

Java语言特别强调准确性但可靠的行为要以性能作为代价这一特点反映在自动收集垃圾严格的运行期检查完整的字节码检查以及保守的运行期同步等等方面对一个解释型的虚拟机来说由于目前有大量平台可供挑选所以进一步阻碍了性能的发挥

先做完它再逐步完善幸好需要改进的地方通常不会太多(Steve McConnell的《About performance》[])

本文的宗旨就是指导大家寻找和优化需要完善的那一部分

.基本方法

只有正确和完整地检测了程序后再可着手解决性能方面的问题

() 在现实环境中检测程序的性能若符合要求则目标达到若不符合则转到下一步

() 寻找最致命的性能瓶颈这也许要求一定的技巧但所有努力都不会白费如简单地猜测瓶颈所在并试图进行优化那么可能是白花时间

() 运用本附录介绍的提速技术然后返回步骤

为使努力不至白费瓶颈的定位是至关重要的一环Donald Knuth[]曾改进过一个程序那个程序把%的时间都花在约%的代码量上在仅一个工作小时里他修改了几行代码使程序的执行速度倍增此时若将时间继续投入到剩余代码的修改上那么只会得不偿失Knuth在编程界有一句名言过早的优化是一切麻烦的根源(Premature optimization is the root of all evil)最明智的做法是抑制过早优化的沖动因为那样做可能遗漏多种有用的编程技术造成代码更难理解和操控并需更大的精力进行维护

.寻找瓶颈

为找出最影响程序性能的瓶颈可采取下述几种方法

安插自己的测试代码

插入下述显式计时代码对程序进行评测

long start = SystemcurrentTimeMillis();

// 要计时的运算代码放在这儿

long time = SystemcurrentTimeMillis() start;

利用Systemoutprintln()让一种不常用到的方法将累积时间打印到控制台窗口由于一旦出错编译器会将其忽略所以可用一个静态最终布尔值(Static final boolean)打开或关闭计时使代码能放心留在最终发行的程序里这样任何时候都可以拿来应急尽管还可以选用更复杂的评测手段但若仅仅为了量度一个特定任务的执行时间这无疑是最简便的方法

SystemcurrentTimeMillis()返回的时间以千分之一秒(毫秒)为单位然而有些系统的时间精度低于毫秒(如Windows PC)所以需要重复n次再将总时间除以n获得准确的时间

JDK性能评测[]

JDK配套提供了一个内建的评测程序能跟蹤花在每个例程上的时间并将评测结果写入一个文件不幸的是JDK评测器并不稳定它在JDK 中能正常工作但在后续版本中却非常不稳定

为运行评测程序请在调用Java解释器的未优化版本时加上prof选项例如

java_g prof myClass

或加上一个程序片(Applet)

java_g prof sunappletAppletViewer applethtml

理解评测程序的输出信息并不容易事实上在JDK 它居然将方法名称截短为字符所以可能无法区分出某些方法然而若您用的平台确实能支持prof选项那么可试试Vladimir Bulatov的HyperPorf[]或者Greg White的ProfileViewer来解释一下结果

特殊工具

如果想随时跟上性能优化工具的潮流最好的方法就是作一些Web站点的常客比如由Jonathan Hardwick制作的Tools for Optimizing Java(Java优化工具)网站

http://wwwcscmuedu/~jch/java/toolshtml

性能评测的技巧

■由于评测时要用到系统时钟所以当时不要运行其他任何进程或应用程序以免影响测试结果

■如对自己的程序进行了修改并试图(至少在开发平台上)改善它的性能那么在修改前后应分别测试一下代码的执行时间

■尽量在完全一致的环境中进行每一次时间测试

■如果可能应设计一个不依赖任何用户输入的测试避免用户的不同反应导致结果出现误差

.提速方法

现在关键的性能瓶颈应已隔离出来接下来可对其应用两种类型的优化常规手段以及依赖Java语言

常规手段

通常一个有效的提速方法是用更现实的方式重新定义程序例如在《Programming Pearls》(编程拾贝)一书中[]Bentley利用了一段小说数据描写它可以生成速度非常快而且非常精简的拼写检查器从而介绍了Doug McIlroy对英语语言的表述除此以外与其他方法相比更好的算法也许能带来更大的性能提升——特别是在数据集的尺寸越来越大的时候欲了解这些常规手段的详情请参考本附录末尾的一般书籍清单

依赖语言的方法

为进行客观的分析最好明确掌握各种运算的执行时间这样一来得到的结果可独立于当前使用的计算机——通过除以花在本地赋值上的时间最后得到的就是标准时间

运算示例标准时间本地赋值i=n; 实例赋值thisi=n;int增值i++;byte增值b++; short增值s++; float增值f++; double增值d++; 空循环while(true) n++; 三元表达式(x<) ?x : x算术调用Mathabs(x);数组赋值a[] = n; long增值l++; 方法调用funct(); throw或catch异常try{ throw e; }或catch(e){}同步方法调用synchMehod(); 新建对象new Object(); 新建数组new int[];

ProNetscape 及JDK 这些相对时间向大家揭示出新建对象和数组会造成最沉重的开销同步会造成比较沉重的开销而一次不同步的方法调用会造成适度的开销参考资源[]和[]为大家总结了测量用程序片的Web地址可到自己的机器上运行它们

特殊情况

■ 字串的开销字串连接运算符+看似简单但实际需要消耗大量系统资源编译器可高效地连接字串但变量字串却要求可观的处理器时间例如假设s和t是字串变量

Systemoutprintln(heading + s + trailer + t);

上述语句要求新建一个StringBuffer(字串缓沖)追加自变量然后用toString()将结果转换回一个字串因此无论磁盘空间还是处理器时间都会受到严重消耗若准备追加多个字串则可考虑直接使用一个字串缓沖——特别是能在一个循环里重复利用它的时候通过在每次循环里禁止新建一个字串缓沖可节省单位的对象创建时间(如前所述)利用substring()以及其他字串方法可进一步地改善性能如果可行字符数组的速度甚至能够更快也要注意由于同步的关系所以StringTokenizer会造成较大的开销

■ 同步在JDK解释器中调用同步方法通常会比调用不同步方法慢经JIT编译器处理后这一性能上的差距提升到倍(注意前表总结的时间显示出要慢倍)所以要尽可能避免使用同步方法——若不能避免方法的同步也要比代码块的同步稍快一些

■ 重复利用对象要花很长的时间来新建一个对象(根据前表总结的时间对象的新建时间是赋值时间的而新建一个小数组的时间是赋值时间的倍)因此最明智的做法是保存和更新老对象的字段而不是创建一个新对象例如不要在自己的paint()方法中新建一个Font对象相反应将其声明成实例对象再初始化一次在这以后可在paint()里需要的时候随时进行更新参见Bentley编着的《编程拾贝》p[]

■ 异常只有在不正常的情况下才应放弃异常处理模块什么才叫不正常呢?这通常是指程序遇到了问题而这一般是不愿见到的所以性能不再成为优先考虑的目标进行优化时将小的trycatch块合并到一起由于这些块将代码分割成小的各自独立的片断所以会妨碍编译器进行优化另一方面若过份热衷于删除异常处理模块也可能造成代码健壮程度的下降

■ 散列处理首先Java 的标准散列表(Hashtable)类需要造型以及特别消耗系统资源的同步处理(单位的赋值时间)其次早期的JDK库不能自动决定最佳的表格尺寸最后散列函数应针对实际使用项(Key)的特征设计考虑到所有这些原因我们可特别设计一个散列类令其与特定的应用程序配合从而改善常规散列表的性能注意Java 集合库的散列映射(HashMap)具有更大的灵活性而且不会自动同步

■ 方法内嵌只有在方法属于final(最终)private(专用)或static(静态)的情况下Java编译器才能内嵌这个方法而且某些情况下还要求它绝对不可以有局部变量若代码花大量时间调用一个不含上述任何属性的方法那么请考虑为其编写一个final版本

■ I/O应尽可能使用缓沖否则最终也许就是一次仅输入/输出一个字节的恶果注意JDK 的I/O类采用了大量同步措施所以若使用象readFully()这样的一个大批量调用然后由自己解释数据就可获得更佳的性能也要注意Java readerwriter类已针对性能进行了优化

■ 造型和实例造型会耗去个单位的赋值时间开销更大的甚至要求上溯继承(遗传)结构其他高代价的操作会损失和恢复更低层结构的能力

■ 图形利用剪切技术减少在repaint()中的工作量倍增缓沖区提高接收速度同时利用图形压缩技术缩短下载时间来自JavaWorld的Java Applets以及来自Sun的Performing Animation是两个很好的教程请记着使用最贴切的命令例如为根据一系列点画一个多边形和drawLine()相比drawPolygon()的速度要快得多如必须画一条单像素粗细的直线drawLine(xyxy)的速度比fillRect(xy)快

■ 使用API类尽量使用来自Java API的类因为它们本身已针对机器的性能进行了优化这是用Java难于达到的比如在复制任意长度的一个数组时arraryCopy()比使用循环的速度快得多

■ 替换API类有些时候API类提供了比我们希望更多的功能相应的执行时间也会增加因此可定做特别的版本让它做更少的事情但可更快地运行例如假定一个应用程序需要一个容器来保存大量数组为加快执行速度可将原来的Vector(矢量)替换成更快的动态对象数组

常规修改

下面是加快Java程序关键部分执行速度的一些常规操作建议(注意对比修改前后的测试结果)

修改成 理由接口抽象类(只需一个父时)接口的多个继承会妨碍性能的优化非本地或数组循环变量本地循环变量根据前表的耗时比较一次实例整数赋值的时间是本地整数赋值时间的但数组赋值的时间是本地整数赋值的

链接列表(固定尺寸)保存丢弃的链接项目或将列表替换成一个循环数组(大致知道尺寸)每新建一个对象都相当于本地赋值参考重复利用对象(下一节)Van Wyk[] p以及Bentley[] px/(或的任意次幂)X>>(或的任意次幂)使用更快的硬件指令

其他建议

■ 将重复的常数计算移至关键循环之外——比如计算固定长度缓沖区的bufferlength

■ static final(静态最终)常数有助于编译器优化程序

■ 实现固定长度的循环

■ 使用javac的优化选项O它通过内嵌staticfinal以及private方法从而优化编译过的代码注意类的长度可能会增加(只对JDK 而言——更早的版本也许不能执行字节查证)新型的Justintime(JIT)编译器会动态加速代码

■尽可能地将计数减至——这使用了一个特殊的JVM字节码

               

上一篇:关于Java23种设计模式的有趣见解[3]

下一篇:JAVA设计模式之事务处理[1]