检测内存泄漏通常采用的方式是检查内存中某些对象的数量是否存在单调递增的现象。这可以通过“在线”实时监控分析的方式或者比较不同时段的内存快照来实现。然而实时监控这样的方案并不一定是可行的,尤其是在生产环境上,得考虑到因此导致的性能消耗;并且内存泄漏的产生通常也是非常偶然的,只有在某些特定条件下才会出现。这篇文章将会介绍一些使用MAT发现内存泄漏的技巧。
首先一定要有足够的数据,这里指的是heap dump文件。可以对JVM进行配置,以实现在发生OutOfMemoryError就自动生成Heap Dump文件(这在同一系列的前两篇文章中说过)。
第二步就是让内存泄漏问题清楚地暴露出来从而容易被捕捉到。这里有一个技巧:试着调整一下应用运行时的最大堆内存,调整到比应用正常运行所需的内存大一些就可以了(建议是一次Full GC后剩余内存的两倍大小)。即使不知道应用运行时到底需要多大的内存,加大堆内存也不是一个坏主意(有可能真的就是内存不够用了,而非是发生了内存泄漏)。这里不讨论分配给一个应用太大的堆内存是好还是坏——这里只是将调整内存作为故障排除的一个临时方案。调整内存后我们会得到什么呢:在得到的Heap Dump文件中,和内存泄漏有关的对象的大小估计会占到堆内存的一半(如果当时设置的是两倍),此时再找导致内存泄漏的原因应该就比较容易了。
现在假设我们已经做好了配置,然后在某一天发生了MMO错误并生成了一个相当大的heap dump文件。接下来该怎么做呢?不管你信不信,接下来要做的事情非常简单。
首先使用MAT打开这个heap dump文件。如果文件非常大的话,第一次打开可能会需要等一段时间,之后再打开这个文件就会非常快了,因为首次打开的时候已经完成了对文件的解析。现在开始尝试找出到底是谁蚕食了我们的内存。点击工具栏上的按钮进入Dominator tree视图:
这里会看到首页的对象图以一个树的形式展现出来。这个树里展示了对象、依赖、它们之间的引用关系以及其他。这里不会详细介绍这个树背后的全部细节,只是列出两个非常重要的特征:
一般发生内存泄漏的时候,都会直接锁定那个体量最大的对象。接下来一步步接近真相:展开最大对象的子树,试着找到retained size最接近最大对象的子节点(通常是一个数组或者集合)。就是这么简单,我们找到了内存泄漏的元凶。如果还想继续探索下去的话,可以尝试探索更深的子节点。
展开看一下:
下一件事就是找到内存泄漏的对象到GCRoots的引用链。选中内存泄漏对象,右键菜单中选择“Paths from the GC roots -> without weak and soft references”即可。
在“Paths from the GC Roots”可以看到我们选中的对象到GCRoot的路径,最顶端就是我们选中的目标,最下方应该就是GCRoots。选的样本不好,估计到GCRoot还得展开好久:
换了一个样本,这下清晰多了:
除了Dominator tree我们还可以看另外两个视图,这两个图表型的视图会更直观,回到首页,在大的饼状图的下方区域有两个子选项Top Comsumers和Leak Suspects。
这个目前就不展开说了,自己打开来看,会有惊喜的。算了,还是看一下Top Consumer的部分吧:
如果所有的问题都能像上面的案例那样容易解决就太好了。有时候仅仅看一次dominator tree是远远不够的。看看下图的案例:这里也提供了足够多的内存让内存泄漏对象去生长,也打开一个覆盖了所有对象的dominator tree,其中就包括内存泄漏对象。但是能看到内存泄漏对象么?在案例一中,所有小的内存泄漏对象都被一个巨大的根对象引用,但是有时候这些相对较小的内存泄漏对象就直接在dominator tree的顶级节点上。尽管这些小的内存泄漏对象数量很多,但是每个对象的Retained Size都比较小,因此不会排在前面。
(手上没有这种场景的数据,目前模拟起来比较困难,所以图片暂时还是用的网上找的。图片中使用的MAT版本较旧,先凑合着看吧)
此时点击工具栏中的“Group by class”按钮会有很大的帮助。
点击按钮后,我们可以看到同一组对象Size的总和,此时再找内存泄漏的原因是不是会更容易一些:
附上测试程序:
案例一的代码:
Java
package com.zhyea.test; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class Test { public static void main(String[] args) throws InterruptedException { new Test().test(); } void test() throws InterruptedException { List<byte []> list = new LinkedList<byte []>(); for (int i = 0; i < 1000000000000L; i++) { list.add(new byte[100000]); Thread.sleep(50L); } } }
package com.zhyea.test; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class Test { public static void main(String[] args) throws InterruptedException { new Test().test(); } void test() throws InterruptedException { List<byte []> list = new LinkedList<byte []>(); for (int i = 0; i < 1000000000000L; i++) { list.add(new byte[100000]); Thread.sleep(50L); } } }
模拟时使用的虚拟机参数:
Vim
-Xmx120m -XX:HeapDumpPath=E:/dumps.bin -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime
-Xmx120m -XX:HeapDumpPath=E:/dumps.bin -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime