转自:http://www.ibm.com/developerworks/cn/java/j-lo-jvm-perf/
Java 平台已无处不在,Java EE、Java SE、Java ME 和 Java Card,Java 的发展为无数程序员提供了工作机会,都是“Java”,然而除了基本的 Java 语法大都一致外,程序员必须基于不同的平台有不同的考虑,学习不同平台的特点:
Java EE 所运行的硬件服务器、操作系统,Java SE 所在 PC 机的体系结构(X86/X64、MAC、SPARC 等),Java ME 所运行的手机或移动设备,Java Card 所在的智能卡芯片类型等;
如是否支持多线程(这似乎是毋庸置疑,但是在 Java Card 平台上,由于计算资源相当有限,多线程目前还不被支持),Java EE 和 Java SE 的虚拟机特性几乎相同,而 Java ME 虚拟机(KVM)根据移动设备的特点进行裁剪和优化,以适应于有限的物理内存和存储空间,而根据设备处理能力的强弱还分为 CDC(Connected Device Configuration,联网设备配置)和 CLDC(Connected Limited Device Configuration,联网受限设备配置),更小设备和智能卡的虚拟机 JCVM(Java Card VM)更是裁剪了许多特性,如多线程、许多复杂数据类型的支持、主动的垃圾收集机制等,这甚至导致了对 Java 语法集的裁剪;
Java EE 和 Java SE 是超集与子集的关系,因为她们所处的计算机平台和操作系统目前很好的兼容,而 Java ME 和 Java Card 与 EE 和 SE 是 Totally different,除了 java.lang.*,部分 java.io.* 等核心类库保留外,其他的 API 和类库完全不同。java.microedition.* 和 javax.microedition.* 表明这是 ME 平台,javacard.* 表明这是 Java Card 平台。同时,由于 EE 和 SE 平台的普及程度和开发者人数,使得之上的第三方库十分海量。深入了解和掌握平台的 API 和库是不同平台程序员进阶的必由之路。
从这个角度上说,Java 在不同的平台之间,并不是“一次编写、处处运行”,考虑应用程序的设计和优化的时候,首先要看是在什么平台上,因为源于以上不同的特点,编程模型、设计模 式,甚至语言集都不尽相同。在这里我们着重考虑 Java EE 和 SE 的视角,但有很多设计、编程原则和习惯对于所有平台的程序员来说,都适用。
Java 虚拟机是支持 Java 语言运行的基础,避开过多的 JVM 和实现的技术细节,我们对基础架构进行了解,是进行应用程序优化必不可少的。如下图所示:
Java 字节码是 JVM 的指令,所有 Java 平台虚拟机有各自的指令集,而大部分指令相同,共 200 条左右,Java Card 虚拟机由于支持的数据类型少,相应的指令较少。部分虚拟机实现商为了优化性能,增加了一些自己特有的指令,当对于 Java 程序员来说,是透明的。下面是一段 Java 方法的字节码示例:
/* 0x000092c4:0x04a7: */ _SCONST_0, /* 0x000092c5:0x04a8: */ _SCONST_0, /* 0x000092c6:0x04a9: */ _INVOKESTATIC, HIGH(0x08e8), LOW(0x08e8), /* 0x000092c9:0x04ac: */ _POP, /* 0x000092ca:0x04ad: */ _INVOKESTATIC, HIGH(0x8046), LOW(0x8046), /* 0x000092cd:0x04b0: */ _IFEQ, 84, /* 0x000092cf:0x04b2: */ _INVOKESTATIC, HIGH(0x8044), LOW(0x8044), /* 0x000092d2:0x04b5: */ _GOTO, 79, /* 0x000092d4:0x04b7: */ _ASTORE, 7, |
当程序计数器中值为 0x000092ca:0x04ad ,表明下一条即将执行字节码为 _INVOKESTATIC, HIGH(0x8046), LOW(0x8046) ,该字节码表明将调用某个静态方法。
Java 语言一大好处就是不用关心对于内存的分配和回收,一切由垃圾收集器搞定。然而这并不代表 Java 程序员可以高枕无忧,再高效的收集器也可能因为滥用而导致性能问题。我们已经知道,Java 程序所涉及的空间分配和回收包括:
来看一段字节码在 Java 栈中的执行示例,100 与 98 相加:
iload_0 // 载入局部变量 0,整型,压入栈中 iload_1 // 载入局部变量 1,整型,压入栈中 iadd // 弹出两个整型数,相加,将结果压入栈 istore_2 // 弹出整型数,存入局部变量 2 |
此外,对于 JVM,还需了解支持的数据类型和它们占用的空间:
虽然各家 JVM 的实现(Sun Hotspot、IBM J9、Oracle JRockit 等)不同,但均采用了按代的垃圾收集机制。垃圾收集就是标识出虚拟机中不被用到的垃圾对象,删除以回收空间。按代垃圾收集算法主要分为三种:
代的划分:
Java 虚拟机都提供了相应的选项来设置各个代所占用区的大小,无论是 Java EE 的服务器应用,还是 Java SE 桌面应用或产品,都需要经过对运行时对象创建和消亡状态的分析,进行这些选项的合理设置,才能获得较好的性能提升,毕竟垃圾收集是一项耗时的工作。读者可 以进一步深入研究相关的虚拟机选项,为自己的应用程序设置优化的数值。
垃圾收集按频率可分为:
垃圾收集运行时,同一个 CPU 上的所有其它线程都将会被阻塞,所以对于 Java 应用程序来说,整个世界似乎停滞了,当整个标记、清除、整理周期完成后,所有应用程序线程得以继续,许多 JVM 实现的垃圾收集机制对多 CPU 的机器环境进行优化,通过同步来实现垃圾收集线程和应用程序线程的并发,使程序获得很好的总体性能。
通过设置虚拟机参数来配置垃圾收集器的行为和堆中不同区的大小分配。不同虚拟机的实现,参数选项不尽相同。IBM J9 虚拟机在 IBM 的从移动设备到企业解决方案中广泛的被使用,本文关于虚拟机选项参数的设定均基于 IBM 的 J9。
了解了垃圾收集以及它对性能的影响后,我们可以根据应用程序的特点来设置 GC 的策略进行有效的优化。相关参数是 -Xgcpolicy:[optthruput | optavgpause | gencon | subpool]
除了设置 GC 策略,最常设置的堆大小参数有:-Xms ,设置堆的初始大小;-Xmx ,设置堆空间的最大值;-Xmn ,设置年轻代空间大小;-Xmo ,设置年老代空间大小。程序员需要根据实际的机器环境和应用本身的特点来设置合理的值。
对虚拟机工作机制的了解能够使我们有把握写出更优雅、更高效的 Java 代码。下面是几条值得参考的设计、编程原则和习惯。
除了由于某些产品兼容性的需要必须使用过去某个虚拟机版本外,建议将开发环境和最终产品部署的虚拟机运行时环境要求更新至最新版本。最新 的版本意味着最新的 API,更好的实现优化。这一点对嵌入式 Java(Java ME 和 Java Card)并不适用,随着移动或智能设备的发行,虚拟机就已经固化在其中,而新发布的虚拟机版本不能像在 EE 和 SE 安装的服务器和 PC 机一样,轻松进行安装。部分移动设备可以通过更新固件和操作系统程序来实现 VM 的版本更新。
这点对于 Java 应用的性能、重用和可维护性尤为重要,设计模式是由大师们总结出的解决典型问题的通用架构,用对象来描述问题域,用设计模式来组织对象之间的行为。在设计 和解决局部问题时,首先要看看抽象出来的问题是否和某个设计模式的目标问题一致。此外,尽可能多的了解虚拟机所支持的 API,看所需的功能是否已有现成的实现可供调用,虚拟机平台实现的 API 大都具有良好的性能。
前面了解到,对于基于栈的 Java 虚拟机,方法的调用和执行伴随着压栈和出栈操作。每个线程有各自独立的栈,由虚拟机来管理栈的大小,但我们应该对它的大小有个概念。栈的大小是把双刃剑, 如果太小,可能会导致栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果过大,就会影响到可创建栈的数量,如果是多线程的应用,就会 导致内存溢出。通过 -Xss 可以设置 Java 栈的最大值,默认值为 256K 。不建议设置该选项为其他值,好的方案是,通过优化程序来减少递归层数、避免过大的循环、减少方法的调用层次,让你的程序尽量“扁平”,用尽可能好的对象间的关系来取代少数对象间深层次的方法调用。
过分依赖垃圾收集有时候会出现严重的性能问题,特别对于在程序运行中伴随着大量大对象创建的情况。好的习惯是显式的释放不用对象的引用, 在下一垃圾收集周期中被回收,这一点常常被 Java 程序员忽视,遗留的引用会导致 GC 无法回收这些逻辑上消亡的对象,看下面代码示例:
public class Stack { private static final int MAXLEN = 10; private Object stk[] = new Object[MAXLEN]; private int stkp = -1; public void push(Object p) { stk[++stkp] = p; } public Object pop1 () { return stk[stkp--]; } public Object pop2 () { Object p = stk[stkp]; stk[stkp--] = null; return p; } } |
示例代码是一个栈结构,栈中存储对象引用,容量为 10,stkp 是栈顶指针,push 方法将对象压入栈中,pop1 和 pop2 弹出栈顶对象。pop1 直接将对象弹出,该对象可能被其它对象使用之后立刻释放,而栈中仍有指向该对象的引用,由于栈可能在程序中长久存在,所以导致弹出的对象不能被回收。 pop2 方法在弹出对象前,将栈原来持有的对象引用置空释放,从而使弹出的对象彻底与栈脱离关系而不影响 GC。对于在程序运行中要大量创建和释放的对象,加强管理是很好的习惯,使用对象池机制是很好的解决方案,根据需要在对象池中创建一批对象,将不用的对象 放回池中,待下次取出使用,这也大大节省了对象的反复创建和销毁时间。
import java.util.HashMap; import java.util.LinkedHashSet; public class ObjectFactory { /** A counter for counting the number of objects in use. */ private static int objInUse = 0; /** A counter for counting the number of objects in pool. */ private static int objInPool = 0; /** The object pool. */ private static HashMap objectPool = new HashMap(); /** The corresponding object pool for a specific class. */ private static LinkedHashSet subObjPool; /** Generate object for use */ public synchronized static Object generate(String className) { Object retObj = null; subObjPool = (LinkedHashSet) objectPool.get(className); if (subObjPool != null && subObjPool.size() < 0) { retObj = subObjPool.iterator().next(); subObjPool.remove(retObj); objInPool--; } else { try { retObj = newObj(className); } catch (InstantiationException ie) { return null; } catch (IllegalAccessException iae) { return null; } catch (ClassNotFoundException cnfe) { return null; } } objInUse++; return retObj; } public synchronized static void drop(Object freeObject) { if (freeObject != null) { subObjPool = (LinkedHashSet) objectPool.get(className); if (subObjPool == null) { subObjPool = new LinkedHashSet(); objectPool.put(className, subObjPool); } if (!subObjPool.contains(freeObject)) { subObjPool.add(freeObject); objInPool++; objInUse--; } } } /** Counts the number of objects which are in use now. */ public static int countObjectInUse() { return objInUse; } /** Checks the current size of the object pool. */ public static int checkPoolSize() { return objInPool; } /** New object for class name. */ private static Object newObj(String className) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Object obj = Class.forName(className).newInstance(); return obj; } } |
Java Profiler 是采用 JMX(Java Management Extensions,Java 资源管理框架)或 JVMPI(Java Virtual Machine Profiler Interface,Java 虚拟机监视程序接口)实现的对 Java 虚拟机中的资源、应用程序对象等进行监试的一类工具。Profiler 工具主要可以监视对象分配和回收、堆空间、线程运行、线程死锁、网络状态等。这为 Java 程序员进行性能分析提供了入手点,通过对程序运行时的状态分析,可以快速的定位问题,从而着手优化。Java Profiler 工具是分析 Java 程序性能的好帮手,但归根结底,性能的提高还依赖于程序员对 Java 虚拟机有一定了解,在此基础上遵循良好的设计和开发原则。这也是 Java 程序员成为真正高手的必由之路。
关于如何使用 profiler 工具,读者可参考相关资源进行深入研究,常用的 Java Profiler 工具有:
本文从 Java 虚拟机的视角出发,剖析了与 Java 应用程序性能相关的因素,通过总结的一些程序员容易忽视的设计、编程原则和习惯,希望对帮助广大 Java 程序员提高性能优化意识和水平有所帮助。