通过JMX控制在full GC前后做heap dump

后一篇: 通过jinfo工具在full GC前后做heap dump

有时候我们想知道一个Java程序在一次full GC的时候到底回收了哪些对象。特别是当full GC看起来很频密但系统看起来却又没有内存泄漏的时候,了解究竟是哪些对象引致了这些GC会对调优有帮助。

做了个简单的例子,讲解一种简单的办法在full GC的前后得到heap dump。本文说的办法只能在HotSpot VM上使用;其它JVM要达到同样的目的或许有其它做法,回头有机会再说。
(同样的工作在JRockit或者J9上做似乎都更容易些…

======================================================================

一般获取heap dump的办法

1、jmap
大家最熟悉的办法或许就是JDK自带的命令行工具jmap了。jmap可以在任何时候连接到一个跑在HotSpot VM的Java进程上,根据需要制作 HPROF格式的heap dump。
jmap -dump:format=b,file=<filename> <pid>

这是最常用的用法。

在Sun的JDK 5和JDK 1.4.2的后期版本中(JDK 5 update 17和JDK 1.4.2 update 16或更高版本),还可以在启动参数里加上 -XX:+HeapDumpOnCtrlBreak,然后通过 ctrl + break或者发生SIGQUIT来让VM生成heap dump。
不过这个参数在Sun JDK 6里不存在;JDK6上直接用jmap更方便些,倒也没关系。
JRockit R28倒是支持使用这个参数。

2、JConsoleVisualVMMAT
这几个工具都封装了heap dump的功能,用起来很方便——只要知道如何让这些工具连接到目标进程上。所以它们通常在本地使用很方便,而要连接远程进程就麻烦一些。
还有别的一些工具也有提供生成heap dump功能的,不过我一下想不起来了就只写了上面仨。
GCMV或许也可以吧…呃,刚看了下,不行。还是得在VM里配置参数来生成heap dump。

JConsole:
通过JMX控制在full GC前后做heap dump_第1张图片

VisualVM:
通过JMX控制在full GC前后做heap dump_第2张图片
通过JMX控制在full GC前后做heap dump_第3张图片

Eclipse Memory Analyzer (MAT):
通过JMX控制在full GC前后做heap dump_第4张图片

3、JMX的API
Sun JDK通过JMX暴露出HotSpotDiagnosticMXBean,可以用于获取VM信息。它支持dumpHeap(String outputFile, boolean live)操作,让Java程序能直接指定路径和是否只要活对象进行heap dump。使用方法可以参考下面的链接: A. Sundararajan's Weblog: Programmatically dumping heap from Java applications

通过Serviceability Agent API也可以做heap dump。事实上jmap的其中一个模式就是包装了SA API的sun.jvm.hotspot.tools.HeapDumper来完成功能的。

4、JVMTI
很老的版本的JVMTI API里曾经有过heap dump函数, 不过后来被去掉了

5、让JVM在一些特定事件发生的时候自动做heap dump
(这就是HotSpot操作起来没有JRockit和J9方便的地方了…)
有时候我们只想在发生OutOfMemoryError的时候让JVM自动生成一个heap dump出来,以便做事后分析。这种时候设置启动参数 -XX:+HeapDumpOnOutOfMemoryError即可。参考下面的文章来了解该参数的一些历史:
Alan Bateman: Heap dumps are back with a vengeance!

HotSpot VM支持其它事件触发heap dump么?参考 官方文档
引用
Flags marked as manageable are dynamically writeable through the JDK management interface (com.sun.management.HotSpotDiagnosticMXBean API) and also through JConsole. In Monitoring and Managing Java SE 6 Platform Applications, Figure 3 shows an example. The manageable flags can also be set through jinfo -flag.

声明为manageable的参数可以在运行时通过JMX修改。与heap dump相关的有以下4个参数:
hotspot/src/share/vm/runtime/globals.hpp
manageable(bool, HeapDumpBeforeFullGC, false,                             \
        "Dump heap to file before any major stop-world GC")               \
                                                                          \
manageable(bool, HeapDumpAfterFullGC, false,                              \
        "Dump heap to file after any major stop-world GC")                \
                                                                          \
manageable(bool, HeapDumpOnOutOfMemoryError, false,                       \
        "Dump heap to file when java.lang.OutOfMemoryError is thrown")    \
                                                                          \
manageable(ccstr, HeapDumpPath, NULL,                                     \
        "When HeapDumpOnOutOfMemoryError is on, the path (filename or"    \
        "directory) of the dump file (defaults to java_pid<pid>.hprof"    \
        "in the working directory)")                                      \

可以看到,除了HeapDumpOnOutOfMemoryError之外,还有 HeapDumpBeforeFullGCHeapDumpAfterFullGC参数,分别用于指定在full GC之前与之后生成heap dump。

顺带一提,前面VisualVM的截图里“Disable Heap Dump on OOME”的功能,就是通过HotSpotDiagnosticMXBean将HeapDumpOnOutOfMemoryError参数设置为false来实现的。

======================================================================

通过JMX API在full GC前后生成heap dump的例子

原始代码放在这里了: https://gist.github.com/978336

很简单,就是演示了:
·获取HotSpotDiagnosticMXBean;
·通过它上面的setVMOption(String name, String value)方法修改 HeapDumpBeforeFullGCHeapDumpAfterFullGC参数为true;
·触发一次full GC;
·将VM参数恢复为false。

为了方便,例子用Groovy来写。要在Groovy Shell中看到GC的日志,可以设置环境变量JAVA_OPTIONS=-XX:+PrintGCDetails,或者是在当前目录放一个.hotspotrc来配置这个参数;我是用的后者。

具体代码:
$ groovysh
[GC [PSYoungGen: 14016K->1312K(16320K)] 14016K->1312K(53696K), 0.0111510 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC [PSYoungGen: 15328K->2272K(30336K)] 15328K->4832K(67712K), 0.0286280 secs] [Times: user=0.02 sys=0.03, real=0.03 secs] 
Groovy Shell (1.7.7, JVM: 1.6.0_25)
Type 'help' or '\h' for help.
----------------------------------------------------------------------------------------------------------------------------
groovy:000> import java.lang.management.ManagementFactory
===> [import java.lang.management.ManagementFactory]
groovy:000> import com.sun.management.HotSpotDiagnosticMXBean
===> [import java.lang.management.ManagementFactory, import com.sun.management.HotSpotDiagnosticMXBean]
groovy:000> 
groovy:000> HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"
===> com.sun.management:type=HotSpotDiagnostic
groovy:000> server = ManagementFactory.platformMBeanServer
[GC [PSYoungGen: 30304K->2288K(30336K)] 32864K->8856K(67712K), 0.0643130 secs] [Times: user=0.16 sys=0.01, real=0.07 secs] 
===> com.sun.jmx.mbeanserver.JmxMBeanServer@7297e3a5
groovy:000> bean = ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean)
===> MXBeanProxy(com.sun.jmx.mbeanserver.JmxMBeanServer@7297e3a5[com.sun.management:type=HotSpotDiagnostic])
groovy:000> bean.setVMOption('HeapDumpBeforeFullGC', 'true')
===> null
groovy:000> bean.setVMOption('HeapDumpAfterFullGC', 'true')
===> null
groovy:000> System.gc()
[GC [PSYoungGen: 10460K->2288K(58368K)] 17028K->9639K(95744K), 0.0166920 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
[Heap Dump: Dumping heap to java_pid16836.hprof ...
Heap dump file created [20066598 bytes in 0.347 secs]
, 0.3514030 secs][Full GC (System) [PSYoungGen: 2288K->0K(58368K)] [PSOldGen: 7351K->9621K(37376K)] 9639K->9621K(95744K) [PSPermGen: 18626K->18626K(37824K)], 0.1324840 secs] [Times: user=0.12 sys=0.02, real=0.14 secs] 
[Heap DumpDumping heap to java_pid16836.hprof.1 ...
Heap dump file created [20013677 bytes in 0.340 secs]
, 0.3398950 secs]===> null
groovy:000> bean.setVMOption('HeapDumpBeforeFullGC', 'false')
===> null
groovy:000> bean.setVMOption('HeapDumpAfterFullGC', 'false')
===> null
groovy:000> quit
Heap
 PSYoungGen      total 58368K, used 9250K [0x00000000edc00000, 0x00000000f1740000, 0x0000000100000000)
  eden space 56064K, 16% used [0x00000000edc00000,0x00000000ee5089b0,0x00000000f12c0000)
  from space 2304K, 0% used [0x00000000f1500000,0x00000000f1500000,0x00000000f1740000)
  to   space 2304K, 0% used [0x00000000f12c0000,0x00000000f12c0000,0x00000000f1500000)
 PSOldGen        total 37376K, used 9621K [0x00000000c9400000, 0x00000000cb880000, 0x00000000edc00000)
  object space 37376K, 25% used [0x00000000c9400000,0x00000000c9d65410,0x00000000cb880000)
 PSPermGen       total 37824K, used 18758K [0x00000000c4200000, 0x00000000c66f0000, 0x00000000c9400000)
  object space 37824K, 49% used [0x00000000c4200000,0x00000000c5451ba8,0x00000000c66f0000)

这样就得到了 java_pid16836.hprof 与 java_pid16836.hprof.1 两个heap dump文件。

把第二个heap dump文件改名为 java_pid16836.1.hprof 之后,用MAT打开这两个heap dump,在第一个文件的histogram试图下可以看到
通过JMX控制在full GC前后做heap dump_第5张图片

目前MAT只支持histogram试图中比较两个heap dump。
点击上方工具条最右边的“<->”按钮,并选上第二个heap dump文件之后,可以看到:
通过JMX控制在full GC前后做heap dump_第6张图片

这样就能很方便的得知这次full GC当中到底收集了多少个什么类型的对象。
实际效果跟手动用jmap -histo比较差不多,不过要精确的在full GC前后手动做些操作不是件简单的事情。

或许会有人想说,为啥MAT不能直接把具体是哪些对象被收集了显示出来呢?
这功能不好做。GC的时候对象可能会被移动,也就是说不能通过地址来将full GC前后的两个heap dump里的记录关联到一起;而HPROF格式也没有记录足够信息让多个heap dump之间能建立起联系。
结果能很方便做比较的就只有按类型做的统计。通常这也能提供有用的头绪去进一步做分析了。

P.S. 如果一个HPROF的heap dump是在开了压缩指针的64位JVM上生成的,那么用MAT查看的时候,里面显示的Shallow Heap和Retained Heap数据都会是错误的。因为HPROF格式只能分辨是32位还是64位的,却没有记录有没有开压缩指针、每个对象实际的大小是多少。这种条件下请不要相信MAT(或其它分析HPROF格式的heap dump的工具)显示的对象大小。

你可能感兴趣的:(jvm,jdk,oracle,ITeye,groovy)