内存泄漏是比较常见的一种应用程序性能问题,一旦发生,则系统的可用内存和性能持续下降;最终将导致内存不足(OutOfMemory),系统彻底宕掉,不能响应任何请求,其危害相当严重。同时,Java堆(Heap)中大量的对象以及对象间之复杂关系,导致内存泄漏问题的探测和分析均比较困难,采用相应的辅助工具是很必要的。
我使用的比较多的是Memory Dump Diagnostic for Java (MDD4J)和IBM HeapAnalyzer,这两个工具都能支持几乎所有JDK版本所生成的堆转储文件,使用前可以在两者的帮助文件中查看一下支持列表。
先说一下IBM HeapAnalyzer,下载之后首先阅读一下readme,这上面详细写了HeapAnalyzer的使用方法。对于我用的2.6版本(最新为3.8),可以在命令行中输入<Java path>java –Xmx[heapsize] –jar ha26.jar <heapdump file>来启动工具并加载heapdump文件。对于比较大的heapdump,将-Xmx设置一个较大的值(大于heapdump的大小),来避免加载过程中的OOM。对于64位机器上产生的超大heapdump,个人机器上分析就不大可能了。
打开heapdump文件后,我一般点击“Analysis”里的“Tree View”,以树的形式从根节点展示内存对象分配的信息
第一行java.lang.ref.Refenrence这个class及它的76个children占用了67%的已用堆大小(31M/46M),它本身仅占用了76bits。双击java.lang.ref.Refenrence,我们可以看到它所引用的两个子节点。其中一个子节点java.lang.ref.Finalizer后的67%指引我们内存泄漏的问题应该在它的引用上。
接下去你可以逐级展开,或者右键点击“Locate a leak suspect”,让HeapAnalyzer帮你找到泄漏可能发生的地方。泄漏一般发生在那些拥有“超乎寻常多”的引用(子节点)的class上,正是这些创建后没有释放、累积了成千上百的对象,造成了OutOfMemory。右键中的“Go to the largest drop subtrees”也是以此为原理而设的,它的解释为:
“Search for total size drop” will find a size drop between the total size of a parent and the biggest total size of child of the parent.
因为出现泄漏的点,每个子节点占用的内存空间不大,但是巨大的数量会导致父节点占用的total size很大。不过反过来寻找到的点都是泄漏发生的地方这种说法是不成立的,否则也不需要我们来分析了。
更多细节的内容,可以看这篇PPT
Memory Dump Diagnostic for Java (MDD4J)则是IBM Support Assistant(ISA)里的一个工具,可以在ISA里加载。它的使用方法和HeapAnalyzer类似,不过它会自动列出“可疑泄漏点”供分析。所依据的,是“分析算法查找父对象与子对象之间对象大小的显著变化。这些发生显著变化的父对象可能是基于数组的容器对象,它们包含大量不断增大的子对象。”
具体的使用方法可以参考《WebSphere Application Server 中的内存泄漏检测与分析:第 2 部分:用于泄漏检测与分析的工具和功能》一文中的实际案例。(不过文中的版本应该比较低,现在能下到的2和3版本有些不同,不过不妨碍使用).
Heapdump工具的使用很简单,难点在于找到“内存泄漏的真正原因”,一般需要通过多个heapdump文件的对比才能找到。
比较分析用于对运行内存泄漏应用程序期间(即可用 Java 堆内存流失时)获取的两个内存转储进行分析。在运行泄漏应用程序的早期触发的内存转储被称为基线内存转储,发生泄漏的应用程序运行一段时间(以允许泄漏程度加大)后触发的内存转储被称为主内存转储。在发生了内存泄漏的情况下,主内存转储可能包含大量对象,而这些对象占用的 Java 堆空间量会比基线内存转储大很多。
为了获得更好的分析结果,建议使主内存转储的触发点与基线内存转储的触发点在时间上拉开一定距离,从而使总耗用堆大小在两个触发点之间大幅增长。
如果发现“主内存转储”中的某个对象数量大大大于“基线内存转储”,那么这个对象一般就是发生泄漏的点。但是要避免在appserver刚启动时就做heapdump,否则会把正常需要分配的对象当作泄漏嫌疑点。比如原先运行3天会发生OOM,那么可以:缩小堆大小,让OOM提早发生;在运行4个小时后每隔4小时手动做一次Heapdump直到OOM发生。这些动作也许不适合在生产环境下进行,可以另建测试环境进行。
之前几篇文章中介绍的分析gc log,和本文讲到的分析heapdump,都是脱机分析法。它们的缺陷就是无法找到代码引起的“性能低下”的原因,正如《用HPjtune分析GC日志》里所看到的那样,系统性能很差,但是没有OOM发生,可用堆在每次full gc后还不断减少的现象不能简单怪罪为内存泄漏,毕竟最后都回收下来了,如果手动做heapdump,可能有问题的对象已被回收,无法得到正确的结果。这种情况下要使用诸如Jprofile这样直接附加到JVM上的工具来监测了。
最后附一下手动生成heapdump的方法,免得事到临头在google。
在Linux/AIX环境下
使用Kill -3 pid命令来调用堆转储.
Windows环境下
1. 找到JVM对象名字。
<wsadmin> set objectName [$AdminControl queryNames WebSphere:type=JVM,process=<servername>,node=<nodename>,*]2. 对JVM MBean调用generateHeapDump操作。
<wsadmin> $AdminControl invoke $objectName generateHeapDump
如果上述方法是没有生成,那么进行下面的设置。
- 访问管理控制台
- 转到“服务器”>“应用程序服务器”> Server1(或者要获取其堆转储的服务器的名称)>“进程定义”>“环境条目”。
- 单击“新建”。
- 在“名称”字段中,输入 IBM_HEAPDUMP(默认是开启的)。在“值”字段中,输入 true。
- 单击“确定”。
- 重复步骤 3 至 5,但将 IBM_HEAPDUMP_OUTOFMEMORY 设置为 true。
- 缺省情况下,将在 ~/WebSphere/AppServer/ 目录中创建内存转储(对于 WebSphere Application Server V6.x 而言,缺省目录是:~/WebSphere/AppServer/profiles/default)。要将堆转储目标定向到另一个目录,请转至“环境条目”,单击“新建”,将 IBM_HEAPDUMPDIR 设置为适当的目录(例如 /heapdumps),然后单击“确定”。
- 单击“保存”,然后在下一个屏幕中再次单击“保存”。
- 转到“服务器”>“应用程序服务器”> server1(或者要获取其堆转储的服务器的名称)>“进程定义”>“Java 虚拟机”。
- 选择“详细垃圾回收”。
- 单击“保存”,然后在下一个屏幕中再次单击“保存”。
- 重新启动服务器。
- 打开命令提示符并转至 /WebSphere/AppServer/bin 目录。
- 通过发出 kill -3 XXXXX 命令来调用堆转储,其中 XXXXX 是进程标识。
如果WebSphere运行在HP-UX上,那么需要
- 访问管理控制台
- 转到“服务器”>“应用程序服务器”> Server1(或者要获取其堆转储的服务器的名称)>“进程定义”>“环境条目”。
- 在“常规参数”中,输入:-Xrunhprof:depth=0,heap=dump,format=a,thread=n,doe=n
- 缺省情况下,将在 ~/Websphere/AppServer/ 目录中创建内存转储。要将堆转储目标定向到另一个目录,请添加 HProf 参数 file=/heapdumpdir/hprof.txt,其中 heapdumpdir 是适当的目录,而 hprof.txt 是适当的文件名。如果创建了多个内存转储,那么将把每个内存转储追加到同一个 hprof.txt 文件中。
- 选中“启用详细垃圾回收方式”。
- 重新启动服务器。
- 通过发出 kill -3 XXXXX 命令创建堆转储,其中 XXXXX 是进程标识。
- 除非另有指定,否则将在 ~/WebSphere/AppServer/ 目录中创建 hprof 转储,并且文件名看起来类似于 java.hprof.txt。
- 关闭应用程序服务器,然后移动 hprof 转储文件。直到正确关闭应用程序服务器之后,hprof 转储文件才完整。
- 注意:请检查是否每个 hprof 转储都包含 HEAP DUMP BEGIN 和 HEAP DUMP END 这两组标记。如果 hprof 转储的这两组标记不齐全,那么表明该转储不完整且不能用于分析。