使用Memory Analyzer分析内存泄漏

概述

检测内存泄漏通常采用的方式是检查内存中某些对象的数量是否存在单调递增的现象。这可以通过“在线”实时监控分析的方式或者比较不同时段的内存快照来实现。然而实时监控的方案通常并不可行,尤其是在生产环境上,得考虑到因此导致的性能消耗;并且内存泄漏的产生通常也是非常偶然的,只有在某些特定条件下才会发生。这篇文章将介绍一些使用MAT发现内存泄漏的技巧。

准备工作

首先一定要有足够的数据,这里指的是heap dump文件。可以对JVM进行配置,以实现一发生OutOfMemoryError就自动生成Heap Dump文件。

第二步就是让内存泄漏问题清楚地暴露出来从而容易被捕捉到。这里有一个技巧:试着调整一下应用运行时的最大堆内存,调整到比应用正常运行所需的内存大一些就可以了(建议是一次Full GC后剩余内存的两倍大小)。即使不知道应用运行时到底需要多大的内存,加大堆内存也不是一个坏主意(有时可能真的就是内存不够用了,而非是发生了内存泄漏)。这里不讨论分配给一个应用太大的堆内存是好还是坏——这里只是将调整内存作为故障排除的一个临时方案。调整内存后我们会得到什么呢:如果确实存在内存泄漏,和内存泄漏有关的对象的大小估计会占到堆内存的一半(如果当时设置的是两倍),此时再找导致内存泄漏的原因应该就比较容易了。

案例一

现在假设我们已经做好了配置,然后某一天发生了MMO错误并生成了一个相当大的heap dump文件。接下来该怎么做呢?说实话,接下来要做的事情非常简单。

首先使用MAT打开这个heap dump文件。如果文件非常大的话,第一次打开可能会需要等一段时间,之后再打开这个文件就会非常快了,因为首次打开的时候已经完成了对文件的解析。现在开始尝试找出到底是谁蚕食了我们的内存。点击工具栏上的按钮进入Dominator tree视图:

dominator tree

这里会看到首页的对象图以一个树的形式展现出来。这个树里展示了对象、依赖、它们之间的引用关系以及其他。这里不会详细介绍这个树背后的全部细节,只是说一下两个重要的指标:

  1. 在树的最顶端(依Retained Heap Size排序)可以看到内存中最大的对象;
  2. 最大的对象就是占用内存最多的对象,它在树中的子节点都是被该对象直接或间接引用的对象(这意味着当这个对象被回收的时候它的子节点对象也会被回收)。

一般发生内存泄漏的时候,都会直接锁定那个最大的对象。接下来一步步接近真相:展开最大对象的子树,试着找到retained size最接近最大对象的子节点(通常是一个数组或者集合)。就是这么简单,我们找到了内存泄漏的元凶。如果还想继续探索下去的话,可以尝试探索更深的子节点。

看一下展开后的结果:

dominator tree top

下一件事就是找到内存泄漏的对象到GCRoots的引用链。选中内存泄漏对象,右键菜单中选择“Paths from the GC roots -> without weak and soft references”即可:

weak reference

在“Paths from the GC Roots”可以看到我们选中的对象到GCRoot的路径,最顶端是我们选中的目标,最下方是GCRoots。选的样本不好,估计到GCRoot还得展开好久:

dominator tree expand

换了一个样本,这下清晰多了:

dominator tree expand2

案例二

如果所有的问题都能像上面的案例那样容易解决就太好了。有时候仅仅看一次dominator tree是远远不够的。看看下图的案例:这里也提供了足够多的内存让内存泄漏对象去生长,也打开一个覆盖了所有对象的dominator tree,其中就包括内存泄漏对象。但是能看到内存泄漏对象么?在案例一中,所有小的内存泄漏对象都被一个巨大的根对象引用,但是有时候这些相对较小的内存泄漏对象就直接在dominator tree的顶级节点上。尽管这些小的内存泄漏对象数量很多,但是每个对象的Retained Size都比较小,因此不会排在前面。

group by class

此时点击工具栏中的“Group by class”按钮会有很大的帮助:

how to group by class

点击按钮后,我们可以看到同一组对象Size的总和,此时再找内存泄漏的原因是不是会更容易一些:

big class

其他

附上测试程序:

模拟时使用的虚拟机参数:

参考文档

Finding Memory Leaks with SAP Memory Analyzer

Shallow heap & Retained heap


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据