MAT分析内存溢出

MAT是分析Java堆内存的一个工具,全称是 The Eclipse Memory Analyzer Tool,用来帮助分析内存泄漏和减少内存消耗。使用MAT分析Java堆快照,可以快速计算出对象的保留大小(Retained Sizes),查找到阻止对象被回收的原因,MAT会自动生成一个包含内存泄漏疑点的报告。

MAT可以从Eclipse网站下载:http://www.eclipse.org/mat/

生成Dump

使用MAT分析的是Heap Dump,也就是堆内存快照,生成快照有以下几种方式:

  • 使用虚拟机参数-XX:+HeapDumpOnOutOfMemoryError,溢出时自动生成快照。
  • 使用jmap命令,jmap -dump:format=b,file=${dir}/jmap.hprof pid
  • 使用MAT导出本地java进程的内存快照,File->Acquire Heap Dump->选择要dump的java进程就可以了。

MAT的使用

生成完dump之后,可以用MAT打开dump出来的快照文件,File -> Open Heap Dump,对dump文件进行分析,生成一个Overview视图:

image

首先会列出堆内存的大小,有多少个类,有多少个对象,以及多少个类加载器。

然后是一个根据对象的Retained Size大小形成的饼状图,鼠标放上去,左侧Inspector视图会显示这个对象的详细信息。

然后是其他功能,比如常用的图表,Histogram直方图、Dominator Tree支配树,还会生成一个分析报告Leak Suspects Report。

基础概念

继续分析之前,先了解几个基础概念。

Shallow Heap 和 Retained Heap

Shallow Heap表示对象本身占用内存的大小,不包含对其他对象的引用,也就是对象头加成员变量(不是成员变量的值)的总和。

Retained Heap是该对象自己的Shallow Heap,并加上从该对象能直接或间接访问到对象的Shallow Heap之和。换句话说,Retained Heap是该对象GC之后所能回收到内存的总和。

把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain的起点。

image

从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。obj2的retained size可以通过相同的方式计算。

对象引用 Reference

关于对象的引用,前面的文章讲到过,划分如下:

  • 强引用(Strong Reference)就是在代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。

  • 软引用(Soft Reference)是用来描述有用非必需的对象。软引用关联的对象,在系统将要发生内存溢出之前,将会对这些对象进行二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。上面所说的“食之无味,弃之可惜”的对象就是属于软引用。

  • 弱引用(Weak Reference)是用来描述非必需的对象,但是比软引用更弱一些,弱引用关联的对象只能生存到下一次垃圾收集发生之前。当下一次垃圾收集时,无论内存是否足够,都会回收掉被弱引用关联的对象。

  • 虚引用(Phantom Reference)也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用存在,完全不会对其生存时间造成任何影响,也无法通过虚引用获得一个对象实例。为对象设置虚引用的目的,就是能在这个对象被收集器回收时收到一个系统通知。

四种引用中,只有强引用是强可达性,根据可达性分析回收内存时,永远不会被回收。

GC Roots 和 引用链

JVM在进行GC的时候是通过使用可达性来判断对象是否存活,通过GC Roots(GC根节点)的对象作为起始点,从这些节点开始进行向下搜索,搜索所走过的路径成为Reference Chain(引用链),当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

如下图所示,对象Object 5、Object 6、Object 7虽然互相关联,但是它们到GC Roots是不可达的,所以它们将被判定为可回收的对象:

image

在 Java 中,可作为 GC Roots 的对象有以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

四种引用,GC Roots以及引用链,可以参考之前的博客文章:《JVM探秘:四种引用、对象的生存与死亡》

Histogram 直方图

点击工具栏上的image图标,打开 Histogram 直方图视图,可以列出每个类产生的实例数量,以及所占用的内存大小和百分比。主界面如下图所示:

image

图中Shallow Heap 和 Retained Heap分别表示对象自身不包含引用的大小和对象自身并包含引用的大小,具体请参考下面 Shallow Heap 和 Retained Heap 部分的内容。默认的大小单位是 Bytes,可以在 Window - Preferences 菜单中设置单位,图中设置的是KB。

通过直方图视图可以很容易找到占用内存最多的几个类(通过Retained Heap排序),还可以通过其他方式进行分组(见下图):

image

如果存在内存溢出,时间久了溢出类的实例数量或者内存占比会越来越多,排名也越来越靠前。可以点击工具类上的image图标进行对比,通过多次对比不同时间点下的直方图对比就很容易把溢出的类找出来。

Dominator Tree 支配树

点击工具栏上的image图标可以打开Dominator Tree(支配树)视图,在此视图中列出了每个对象(Object Instance)与其引用关系的树状结构,同时包含了占用内存的大小和百分比。

image

通过Dominator Tree视图可以很容易的找出占用内存最多的几个对象(根据Retained Heap或Percentage排序),和Histogram类似,也可以通过不同的方式进行分组显示。

定位溢出源

Histogram视图和Dominator Tree视图的角度不同,前者是基于类的角度,后者是基于对象实例的角度,并且可以更方便的看出其引用关系。

首先,在两个视图中找出疑似溢出的对象或者类(可以通过Retained Heap排序,并且可以在Class Name中输入正则表达式的关键词只显示指定的类名),然后右键选择Path To GC Roots(Histogram中没有此项)或Merge Shortest Paths to GC Roots,然后选择 exclude all phantom/weak/soft etc. reference:

image

GC Roots意为GC根节点,其含义见上面的 GC Roots和引用链部分,后面的 exclude all phantom/weak/soft etc. reference 意思是排除虚引用、弱引用和软引用,即只剩下强引用,因为除了强引用之外,其他的引用都可以被JVM GC掉,如果一个对象始终无法被GC,就说明有强引用存在,从而导致在GC的过程中一直得不到回收,最终就内存溢出了。

通过结果就可以很方便的定位到具体的代码,然后分析是什么原因无法释放该对象,比如被缓存了或者没有使用单例模式等等。

举例,如果是这样的执行结果:

image

上图中保留了大量的VelocitySqlBulder的外部引用,后来查看了代码,原来每次调用的时候都实例化一个新的对象,由于VelocitySqlBulder类是无状态的工具类,因此修改为单例方式就可以解决这个问题。

后续观察

根据上面分析的结果对问题进行处理之后,再对照之前的操作,看看对象是否还再持续增长,如果没有就说明这个地方的问题已经解决了。

最后再用 jstat 持续跟踪一段时间,看看Old和Perm区的内存是否最终稳定在一个范围之内,如果长时间稳定在一个范围说明溢出问题得到了解决,否则还要继续进行分析和处理,一直到稳定为止。