有時候我們想知道一個Java程序在一次full GC的時候到底回收了哪些對象。特別是當(dāng)full GC看起來很頻密但系統(tǒng)看起來卻又沒有內(nèi)存泄漏的時候,了解究竟是哪些對象引致了這些GC會對調(diào)優(yōu)有幫助。
做了個簡單的例子,講解一種簡單的辦法在full GC的前后得到heap dump。本文說的辦法只能在HotSpot VM上使用;其它JVM要達(dá)到同樣的目的或許有其它做法,回頭有機(jī)會再說。
(同樣的工作在JRockit或者J9上做似乎都更容易些…
)
======================================================================
一般獲取heap dump的辦法1、jmap大家最熟悉的辦法或許就是JDK自帶的命令行工具jmap了。jmap可以在任何時候連接到一個跑在HotSpot VM的Java進(jìn)程上,根據(jù)需要制作
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或更高版本),還可以在啟動參數(shù)里加上
-XX:+HeapDumpOnCtrlBreak,然后通過
ctrl + break或者發(fā)生SIGQUIT來讓VM生成heap dump。
不過這個參數(shù)在Sun JDK 6里不存在;JDK6上直接用jmap更方便些,倒也沒關(guān)系。
JRockit R28倒是支持使用這個參數(shù)。
2、JConsole、VisualVM、MAT這幾個工具都封裝了heap dump的功能,用起來很方便——只要知道如何讓這些工具連接到目標(biāo)進(jìn)程上。所以它們通常在本地使用很方便,而要連接遠(yuǎn)程進(jìn)程就麻煩一些。
還有別的一些工具也有提供生成heap dump功能的,不過我一下想不起來了就只寫了上面仨。
GCMV或許也可以吧…呃,剛看了下,不行。還是得在VM里配置參數(shù)來生成heap dump。
JConsole:
VisualVM:
Eclipse Memory Analyzer (MAT):
3、JMX的APISun JDK通過JMX暴露出HotSpotDiagnosticMXBean,可以用于獲取VM信息。它支持dumpHeap(String outputFile, boolean live)操作,讓Java程序能直接指定路徑和是否只要活對象進(jìn)行heap dump。使用方法可以參考下面的鏈接:
A. Sundararajan's Weblog: Programmatically dumping heap from Java applications通過Serviceability Agent API也可以做heap dump。事實(shí)上jmap的其中一個模式就是包裝了SA API的sun.jvm.hotspot.tools.HeapDumper來完成功能的。
4、JVMTI很老的版本的JVMTI API里曾經(jīng)有過heap dump函數(shù),
不過后來被去掉了。
5、讓JVM在一些特定事件發(fā)生的時候自動做heap dump(這就是HotSpot操作起來沒有JRockit和J9方便的地方了…)
有時候我們只想在發(fā)生OutOfMemoryError的時候讓JVM自動生成一個heap dump出來,以便做事后分析。這種時候設(shè)置啟動參數(shù)
-XX:+HeapDumpOnOutOfMemoryError即可。參考下面的文章來了解該參數(shù)的一些歷史:
Alan Bateman: Heap dumps are back with a vengeance!HotSpot VM支持其它事件觸發(fā)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的參數(shù)可以在運(yùn)行時通過JMX修改。與heap dump相關(guān)的有以下4個參數(shù):
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之外,還有
HeapDumpBeforeFullGC與
HeapDumpAfterFullGC參數(shù),分別用于指定在full GC之前與之后生成heap dump。
順帶一提,前面VisualVM的截圖里“Disable Heap Dump on OOME”的功能,就是通過HotSpotDiagnosticMXBean將HeapDumpOnOutOfMemoryError參數(shù)設(shè)置為false來實(shí)現(xiàn)的。
======================================================================
通過JMX API在full GC前后生成heap dump的例子原始代碼放在這里了:
https://gist.github.com/978336很簡單,就是演示了:
·獲取HotSpotDiagnosticMXBean;
·通過它上面的setVMOption(String name, String value)方法修改
HeapDumpBeforeFullGC與
HeapDumpAfterFullGC參數(shù)為true;
·觸發(fā)一次full GC;
·將VM參數(shù)恢復(fù)為false。
為了方便,例子用Groovy來寫。要在Groovy Shell中看到GC的日志,可以設(shè)置環(huán)境變量JAVA_OPTIONS=-XX:+PrintGCDetails,或者是在當(dāng)前目錄放一個.hotspotrc來配置這個參數(shù);我是用的后者。
具體代碼:
- $ 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試圖下可以看到
目前MAT只支持histogram試圖中比較兩個heap dump。
點(diǎn)擊上方工具條最右邊的“<->”按鈕,并選上第二個heap dump文件之后,可以看到:
這樣就能很方便的得知這次full GC當(dāng)中到底收集了多少個什么類型的對象。
實(shí)際效果跟手動用jmap -histo比較差不多,不過要精確的在full GC前后手動做些操作不是件簡單的事情。
或許會有人想說,為啥MAT不能直接把具體是哪些對象被收集了顯示出來呢?
這功能不好做。GC的時候?qū)ο罂赡軙灰苿?,也就是說不能通過地址來將full GC前后的兩個heap dump里的記錄關(guān)聯(lián)到一起;而HPROF格式也沒有記錄足夠信息讓多個heap dump之間能建立起聯(lián)系。
結(jié)果能很方便做比較的就只有按類型做的統(tǒng)計(jì)。通常這也能提供有用的頭緒去進(jìn)一步做分析了。
P.S. 如果一個HPROF的heap dump是在開了壓縮指針的64位JVM上生成的,那么用MAT查看的時候,里面顯示的Shallow Heap和Retained Heap數(shù)據(jù)都會是錯誤的。因?yàn)镠PROF格式只能分辨是32位還是64位的,卻沒有記錄有沒有開壓縮指針、每個對象實(shí)際的大小是多少。這種條件下請不要相信MAT(或其它分析HPROF格式的heap dump的工具)顯示的對象大小。
(###)