Oracle的Hotspot VM在内存管理方面提供了非常多的选项,一方面说明Hotspot VM在内存管理这块功能非常完善,定制能力和适应能力非常强大,提供了很多内存管理算法的实现以及相关的开关,用于协助用户完成内存管理及其调优;另外也说明内存管理确实不好做,虽然Oracle尽力为用户提供通用的能力,满足绝大多数用户的需求,但通用方案的表现暂时还不能满足特定产品实际部署的需要,还是需要用户根据自己的实际情况来对各项参数进行调整,以达到用户期望的最优的效果。所谓成也萧何,败也萧何,定制能力强带来了一些烦人的问题,首先是可用的选项太多,不知道如何选择;解决了选择的问题之后,选项的取值问题又摆在面前。JVM是一个复杂的系统,选项的选择和取值的选择都会对运行在JVM之上的应用产生影响,因此如何评估选项的选取、选项的取值对应用的影响,就变成了一个实在问题。
下面的操作都期望运行环境安装了JDK,并且最好与产品使用的JRE版本一致。对于分析部署在Linux环境下的产品来说,图形界面通常是奢侈品,命令行工具要省事的多,并且不会对产品的运行产生太大的影响。JVM对于使用Java做为日常开发的程序来说可以理解为黑盒子,但是JVM的开发团队还是提供了不少的方法和资料来帮助程序员来理解这个盒子,至于理解多少,就要看个人的努力程度了。
jinfo命令即是为此需求而设计的,执行jinfo <pid>就可以查看到pid对应的Java进程在启动时传给JVM的参数。以Win32平台为例,jinfo的帮助如下:
Usage: jinfo [option] <pid> (to connect to running process) jinfo [option] <executable <core> (to connect to a core file) jinfo [option] [server_id@]<remote server IP or hostname> (to connect to remote debug server) where <option> is one of: -flag <name> to print the value of the named VM flag -flag [+|-]<name> to enable or disable the named VM flag -flag <name>=<value> to set the named VM flag to the given value -flags to print VM flags -sysprops to print Java system properties <no option> to print both of the above -h | -help to print this help message从帮助信息可以看出,jinfo命令还有其它复杂的用法,这里不再赘述。另外,在Java进程启动前,为JVM传入选项-XX:+PrintCommandLineFlags,可以将实际传入JVM的全部参数输出到标准输出,以供调试应用的启动脚本。
jmap命令非常强大,除了可以查看当前JVM的Java堆内存分布外,还可以导出堆的快照信息以待事后分析堆内的分布或者对象情况。执行jmap -heap <pid>即可观察当前进程ID为pid的Java应用的堆分布情况。以Win32平台为例,jmap命令的帮助信息如下:
Usage: jmap [option] <pid> (to connect to running process) jmap [option] <executable <core> (to connect to a core file) jmap [option] [server_id@]<remote server IP or hostname> (to connect to remote debug server) where <option> is one of: <none> to print same info as Solaris pmap -heap to print java heap summary -histo[:live] to print histogram of java object heap; if the "live" suboption is specified, only count live objects -permstat to print permanent generation statistics -finalizerinfo to print information on objects awaiting finalization -dump:<dump-options> to dump java heap in hprof binary format dump-options: live dump only live objects; if not specified , all objects in the heap are dumped. format=b binary format file=<file> dump heap to <file> Example: jmap -dump:live,format=b,file=heap.bin <pid> -F force. Use with -dump:<dump-options> <pid> or -histo to force a heap dump or histogram when <pid> does not respond. The "live" suboption is not supported in this mode. -h | -help to print this help message -J<flag> to pass <flag> directly to the runtime system同jinfo命令,jmap命令的复杂用法此处不再赘述。
我比较喜欢使用jstat -gcutil <pid> 15000 1000的组合,这个组合的意思是对进程ID为pid的Java应用进行观察,每15秒输出一次报告,总共输出1000次,输出报告的格式以-gcutil选项来控制。报告的格式还有其它样式,满足不同场景、不同使用程序员使用习惯的需求,具体列表可以使用jstat -options命令来查阅,样例如下:
jstat -options -class -compiler -gc -gccapacity -gccause -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -printcompilation对于jstat命令的其它应用,同样可以通过查阅帮助来完成,这里仍然以Win32平台为例,如下:
Usage: jstat -help|-options jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]] Definitions: <option> An option reported by the -options option <vmid> Virtual Machine Identifier. A vmid takes the following form: <lvmid>[@<hostname>[:<port>]] Where <lvmid> is the local vm identifier for the target Java virtual machine, typically a process id; <hostname> is the name of the host running the target Java virtual machine; and <port> is the port number for the rmiregistry on the target host. See the jvmstat documentation for a more complete description of the Virtual Machine Identifier. <lines> Number of samples between header lines. <interval> Sampling interval. The following forms are allowed: <n>["ms"|"s"] Where <n> is an integer and the suffix specifies the units as milliseconds("ms") or seconds("s"). The default units are "ms". <count> Number of samples to take before terminating. -J<flag> Pass <flag> directly to the runtime system.
这个方法需要在Java应用启动时为JVM传入几个参数,让JVM在运行时刻将垃圾回收器的工作情况输出到外部文件里,供进一步的分析,选项是-Xloggc:<logfile>。通常情况下,为了更好的分析垃圾回收器在工作时的行为,还需要增加另外几个选项,使JVM输出更多的信息,如-XX:+PrintGCTimeStamps和-XX:+PrintGCDetails。可以设计一段简短的代码来学习阅读GC日志的输出格式,样例代码如下。
public class GCTest { public static final int K_SIZE = 1024 * 1024; public static void main(final String[] args) throws Exception { byte[] a1, a2, a3; final byte[] a4; a1 = new byte[4 * K_SIZE]; a2 = new byte[4 * K_SIZE]; a1 = null; a3 = new byte[4 * K_SIZE]; a4 = null; } }传给JVM的参数为-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8,输出的GC日志如下。
[GC [DefNew: 4707K->404K(9216K), 0.0101398 secs] 4707K->4500K(19456K), 0.0253303 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] [GC [DefNew: 4664K->404K(9216K), 0.0093135 secs] 8760K->8596K(19456K), 0.0093691 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] Heap def new generation total 9216K, used 5779K [0x31540000, 0x31f40000, 0x31f40000) eden space 8192K, 65% used [0x31540000, 0x31a7f9d0, 0x31d40000) from space 1024K, 39% used [0x31d40000, 0x31da5330, 0x31e40000) to space 1024K, 0% used [0x31e40000, 0x31e40000, 0x31f40000) tenured generation total 10240K, used 8192K [0x31f40000, 0x32940000, 0x32940000) the space 10240K, 80% used [0x31f40000, 0x32740020, 0x32740200, 0x32940000) compacting perm gen total 12288K, used 151K [0x32940000, 0x33540000, 0x36940000) the space 12288K, 1% used [0x32940000, 0x32965f18, 0x32966000, 0x33540000) ro space 10240K, 45% used [0x36940000, 0x36dc73d8, 0x36dc7400, 0x37340000) rw space 12288K, 54% used [0x37340000, 0x379cacc8, 0x379cae00, 0x37f40000)日志内容不多,但信息不少,从中可以看出,DefNew表明使用的是线性垃圾回收器,在程序执行过程中总共有两次回收操作,下面分段说明格式的含义。
[GC [DefNew: 4707K->404K(9216K), 0.0101398 secs] 4707K->4500K(19456K), 0.0253303 secs] [Times: user=0.01 sys=0.00, real=0.03 secs]新生代从4707K降至404K,使用了约0.01秒的时间,而堆内对象占用的空间从4707K降至4500K,说明有4K左右的对象被移动到了老生代,总过程占用了0.025秒。
GC日志的最后一部分是程序退出前的内存分布状态,包括了新生代、老生代、持久代各自占用的总空间及使用率,其中新生代中eden、from、to三个区间的大小和使用率。
分析GC日志比较花费时间,效率不高,在条件允许的时候,如开发阶段,还是应当多多使用现成的工具,如随JDK自带的Java VisualVM工具,用法比较简单,同样不再赘述。
BTW:
做程序员时间久了人就会变得比较挑剔,使用第三方的软件时都会莫名其妙的希望软件又强大又简单,对普通人来说这二者是同义词,但对于软件的开发者来说,强大的第三方软件通常都意味着较高的学习代价。我并不是不喜欢通用的软件工具,只是有时会觉得,让通用的特性贴合我的特别场景,总是特别吃力、费劲。
深入了解JVM在内存管理上的机制其实并不能对程序员实际编码的能力有多少实质上的提升,但是对于希望有进一步发展机会的程序员来说,了解语言背后的内幕,有助于更好的利用语言内建的能力,不论是为了让自己少吃语言的陷阱还是为了项目本身,其实都有好处。