JAVA代码性能调优

寻找瓶颈

1、安插自己的测试代码

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

long start = System.currentTimeMillis();
// 要计时的运算代码放在这儿
long time = System.currentTimeMillis() - start;

可用一个“静态最终布尔值”(Static final boolean)打开或关闭计时,使代码能放心留在最终发行的程序里,这样任何时候都可以拿来应急。

2、JDK性能评测(不稳定,不推荐使用)

3、性能评测技巧:

  • 由于评测时要用到系统时钟,所以当时不要运行其他任何进程或应用程序,以免影响测试结果。
  • 如对自己的程序进行了修改,并试图(至少在开发平台上)改善它的性能,那么在修改前后应分别测试一下代码的执行时间。
  • 尽量在完全一致的环境中进行每一次时间测试。
  • 如果可能,应设计一个不依赖任何用户输入的测试,避免用户的不同反应导致结果出现误差。

优化建议

字串的开销:字串连接运算符+看似简单,但实际需要消耗大量系统资源。编译器可高效地连接字串,但变量字串却要求可观的处理器时间。例如,假设s 和t 是字串变量:System.out.println("heading" + s + "trailer" + t);上述语句要求新建一个StringBuffer(字串缓冲),追加自变量,然后用toString()将结果转换回一个字符串。因此,无论磁盘空间还是处理器时间,都会受到严重消耗。若准备追加多个字串,则可考虑直接使用一个字串缓冲——特别是能在一个循环里重复利用它的时候。通过在每次循环里禁止新建一个字串缓冲,可节省980 单位的对象创建时间(如前所述)。利用substring()以及其他字串方法,可进一步地改善性能。如果可行,字符数组的速度甚至能够更快。也要注意由于同步的关系,所以StringTokenizer 会造成较大的开销。

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

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

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

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

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

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

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

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

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

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

其他建议

将重复的常数计算移至关键循环之外——比如计算固定长度缓冲区的buffer.length。

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

实现固定长度的循环。

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

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



留言