将飞行记录(Flight Recordings)启用堆分析功能能够帮助我们解决内存泄露的问题,它会展现堆中的对象以及随着时间推移,哪些对象增长最快。要启用堆分析功能,你可以使用Java Mission Control并选中“Heap Statistics”,这个选项可以通过“Window->Flight Recording Template Manager”找到,如下所示:
或者手动编辑.jfc文件,将heap-statistics-enabled设置为true。
<event path="vm/gc/detailed/object_count">
<setting control="heap-statistics-enabled">true</setting>
<setting>everyChunk</setting>
</event>
飞行记录可以通过如下的方式来创建:
JVM Flight Recorder选项,比如:
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
-XX:StartFlightRecording=delay=20s,duration=60s,name=MyRecording,
filename=C:\TEMP\myrecording.jfr,settings=profile
Java诊断命令:jcmd
jcmd 7060 JFR.start name=MyRecording settings=profile delay=20s duration=2m filename=c:\TEMP\myrecording.jfr
Java任务控制(Java Mission Control)
飞行记录器只能帮我们确定哪种类型的对象出现了泄露,但是想要找到是什么原因导致了这些对象泄露,我们还需要堆转储。
Java堆:分析诊断数据 堆转储分析堆转储可以使用如下的工具进行分析:
我使用Eclipse MAT较多,我发现在分析堆转储时,它是非常有用的。
MAT有一些高级的特性,包括直方图以及与其他的直方图进行对比的功能。这样的话,就能清晰地看出内存中哪些内容在增长并且能够看到Java堆中占据空间最大的是什么内容。我非常喜欢的一个特性是“Merge Shortest Paths to GC Roots(合并到GC Root的最短路径)”,它能够帮助我们查找无意中所持有的对象的跟踪痕迹。比如,在下面的引用链中,ThreadLocalDateFormat对象被ThreadLocalMap$Entry对象的“value”字段所持有。只有当ThreadLocalMap$Entry从ThreadLocalMap中移除之后,ThreadLocalDateFormat才能被回收。
weblogic.work.ExecuteThread @ 0x6996963a8 [ACTIVE] ExecuteThread: '203' for queue: 'weblogic.kernel.Default (self-tuning)' Busy Monitor, Thread| 1 | 176 | 40 | 10,536
'- threadLocals java.lang.ThreadLocal$ThreadLocalMap @ 0x69c2b5fe0 | 1 | 24 | 40 | 7,560
'- table java.lang.ThreadLocal$ThreadLocalMap$Entry[256] @ 0x6a0de2e40 | 1 | 1,040 | 40 | 7,536
'- [116] java.lang.ThreadLocal$ThreadLocalMap$Entry @ 0x69c2ba050 | 1 | 32 | 40 | 1,088
'- value weblogic.utils.string.ThreadLocalDateFormat @ 0x69c23c418 | 1 | 40 | 40 | 1,056
通过这种方式,我们可以看到堆中增长最快的罪魁祸首,并且看到内存中哪里出现了泄露。
Java任务控制Java任务控制可以在JDK的<jdk>/bin文件夹中找到。启用Heap Statistics功能之后所收集到的飞行记录能够极大地帮助我们解决内存泄露问题。我们可以在Memory->Object Statistics中查看对象的分析信息。这个视图将会展现对象的直方图,包括每个对象类型所占据的堆的百分比。它能够展现堆中增长最快的对象,在大多数情况下,也就直接对应了内存泄露的对象。
终结器所导致的OutOfMemoryError滥用终结器(finalizer)可能也会造成OutOfMemoryError。带有终结器的对象(也就是含有finalize()方法)会延迟它们所占有空间的回收。在回收这些实例并释放其堆空间之前,终结器线程(finalizer thread)需要调用它们的finalize()方法。如果终结者线程的处理速度比不上要终结对象的增加速度(添加到终结者队列中以便于调用其finalize()方法)的话,那么即便终结器队列中的对象都有资格进行回收,JVM可能也会出现OutOfMemoryError。因此,非常重要的一点就是确保不要因为大量对象等待(pending)终结而造成内存耗尽。
我们可以使用如下的工具来监控等待终结的对象数量:
JConsole
我们可以连接JConsole到一个运行中的进程,然后在VM Summary页面查看等待终结的对象数量,如下图所示。