08、JVM 调优实战 - JVM 利用MAT 工具分析内存泄漏详解

今天分析MAT 工具

一、先分析 VisualVM

1、 我们前面讲过,我们可以使用jmap–histo这种命令去分析哪些对象占据着我们的堆空间但是那是比较容易分析的问题,如果是遇到内存情况比较复杂的情况,命令的方式是看不出来的,这个时候我们必须要借助一下工具当然前提是通过jmap命令把整个堆内存的数据dump下来;

 

2、内存分析工具 VisualVM

 

3、 这个是上节的那个案例抛出了OOM后到处的内存的dump日志信息,我们可以导入;

 

 

 

 

VisualVm 属于比较寒酸的工具,基本上跟 jmap 之类的命令没多少区别,它只是可以事后看,通过 dump 信息来看,里面没有多少可以做分析的功能

二、MAT 简介

1、 MAT工具是基于Eclipse平台开发的,本身是一个Java程序,是一款很好的内存分析工具,所以如果你的堆快照比较大的话,则需要一台内存比较大的分析机器,并给MAT本身加大初始内存,这个可以修改安装目录中的MemoryAnalyzer.ini文件;

 

 

 

柱状图:

 

2、MAT 中的 Incoming/Outgoing References

在柱状图中,我们看到,其实它显示的东西跟 jmap –histo 非常相似的,也就是类、实例、空间大小。 但是 MAT 有一个专业的概念,这个可以显示对象的引入和对象的引出。 在 Eclipse MAT 中,当右键单击任何对象时,将看到下拉菜单。如果选择 “ListObjects” 菜单项,则会注意到两个选项:

with incoming references 对象的引入

with outgoing references 对象的引出

3、案例解释理解

 

代码中对象和引用关系如下:

对象A 和对象 B 持有对象 C 的引用

对象C 持有对象 D 和对象 E 的引用

 

4、 我们具体分析对象C的Incomingreferences和Outgoingreferences;

MAT 连接上( MAT 不单单只打开 dump 日志,也可以打开正在运行的 JVM 进程,跟 arthas 有点类似,效果是一样的,只是一个是动态的,一个是日 志导出那个时刻的)

 

 

 

 

 

 

 

对象C 的 incoming references 为对象 A 、对象 B 和 C 的类对象( class )

我们再来分析下 outgoing reference

 

 

对象C 的 outgoing references 为对象 D 、对象 E 和 C 的类对象( class ) 这个 outgoing references 和 incoming references 非常有用,因为我们做 MAT 分析一般时对代码不了解,排查内存泄漏也好,排查问题也好,垃圾回收中有 一个很重要的概念,可达性分析算法,那么根据这个引入和引出,我就可以知道这些对象的引用关系,在 MAT 中我们就可以知道比如 A,B,C,D,E,F 之间的 引用关系图,便于做具体问题的分析。

5、MAT 中的浅堆与深堆

浅堆( shallow heap 代表了对象本身的内存占用,包括对象自身的内存占用,以及“为了引用”其他对象所占用的内存。

深堆( Retained heap 是一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同,深堆指的是一个对象被垃圾回 收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集(Retained Set )

需要说明一下: JAVA 对象大小 = 对象头 + 实例数据 + 对齐填充

非数组类型的对象的 shallow heap

shallow_size= 对象头 + 各成员变量大小之和 + 对齐填充

其中,各成员变量大小之和就是实例数据,如果存在继承的情况,需要包括父类成员变量

数组类型的对象的 shallow size

shallow size= 对象头 + 类型变量大小 * 数组长度 + 对齐填充,如果是引用类型,则是四字节或者八字节( 64 位系统),

如果是 boolean 类型,则是一个字节

注意:这里 类型变量大小 * 数组长度 就是实例数据,强调是变量不是对象本身

对象的内存布局:

 

案例分析:

 

对象A 持有对象 B 和 C 的引用。

对象B 持有对象 D 和 E 的引用。

对象C 持有对象 F 和 G 的引用。

Shallow Heap 大小

请记住: 对象的 Shallow heap 是其自身在内存中的大小

引用变动的影响

在下面的示例中,让对象 H 开始持有对 B 的引用。注意对象 B 已经被对象 A 引用了。

 

在这种情况下,对象 A Retained heap 大小将从之前的 70 减小到 40 个字节。

如果对象 A 被垃圾回收了,则将仅会影响 C 、 F 和 G 对象的引用。因此,仅对象 C 、 F 和 G 将被垃圾回收。另一方面,由于 H 持有对 B 的活动引 用,因此对象 B 、 D 和 E 将继续存在于内存中。因此,即使 A 被垃圾回收, B 、 D 和 E 也不会从内存中删除。因此, A 的 Retained heap 大小为: = A 的 shallow heap 大小 + C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes = 40 bytes.

小结: 我们可以看到在进行内存分析时,浅堆和深堆是两个非常重要的概念,尤其是深堆,影响着回收这个对象能够带来的垃圾回收的效果,所以在内 存分析中,我们往往会去找那些深堆比较的大的对象,尤其是那些浅堆比较小但深堆比较大的对象,这些对象极有可能是问题对象。

6、使用 MAT 进行内存泄漏检测

如果问题特别突出,则可以通过 Find Leaks 菜单快速找出问题。

运行以下代码, 我们开始跑程序:

 

 

 

 

这里一个名称叫做 king-thread 的线程,持有了超过 99% 的对象,数据被一个 HashMap 所持有。 这个就是内存泄漏的点,因为我代码中对线程进行了标识,所以像阿里等公司的编码规范中为什么一定要给线程取名字,这个是有依据的,如果不取名 字的话,这种问题的排查将非常困难。

 

 

所以,如果是对于特别明显的内存泄漏,在这里能够帮助我们迅速定位,但通常内存泄漏问题会比较隐蔽,我们需要做更加复杂的分析。

7、支配树视图

支配树列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具 可以帮助我们定位对象间的引用情况,以及垃圾回收时的引用依赖关系。

支配树视图对数据进行了归类,体现了对象之间的依赖关系。我们通常会根据“深堆”进行倒序排序,可以很容易的看到占用内存比较高的几个对象, 点击前面的箭头,即可一层层展开支配关系(依次找深堆明显比浅堆大的对象)。

 

 

 

从上图层层分解,我们也知道,原来是 king-thread 的深堆和浅堆比例很多(深堆比浅堆多很多、一般经验都是找那些浅堆比较小,同时深堆比较大的对 象)

1、 一个浅堆非常小的king-thread持有了一个非常大的深堆;

2、 这个关系来源于一个HashMap;

3、 这个map中有对象A,同时A中引用了B,B中引用了C;

4、 最后找到C中里面有一个ArrayList引用了一个大数据的数组;

经过分析,内存的泄漏点就在此。一个线程长期持有了 200 个这样的数组 , 有可能导致内存泄漏。

8、MAT 中内存对比

我们对于堆的快照,其实是一个“瞬时态”,有时候仅仅分析这个瞬时状态,并不一定能确定问题,这就需要对两个或者多个快照进行对比,来确定一

个增长趋势。

我们导出两份 dump 日志,分别是上个例子中循环次数分别是 10 和 100 的两份日志

 

 

导出dump文件 对比:打开柱状图,要注意通过包来分组快速找到我们项目中对象的类

 

 

 

 

经过内存日志的对比,分析出来这个类的对象的增长,也可以辅助到问题的定位(快速增加的地方有可能存在内存泄漏)

9、线程视图

想要看具体的引用关系,可以通过线程视图。线程在运行中是可以作为 GC Roots 的。我们可以通过线程视图展示了线程内对象的引用关系,以及方法调 用关系,相对比 jstack 获取的栈 dump ,我们能够更加清晰地看到内存中具体的数据。

我们找到了 king-thread ,依次展开找到 holder 对象,可以看到内存的泄漏点

 

 

还有另外一段是陷入无限循环,这个是相互引用导致的(进行问题排查不用被这种情况给误导了,这样的情况一般不会有问题 --- 可达性分析算法的解决了

相互引用的问题)

 

 

 

10、柱状图视图

柱状图视图,可以看到除了对象的大小,还有类的实例个数。结合 MAT 提供的不同显示方式,往往能够直接定位问题。也可以通过正则过滤一些信息, 我们在这里输入 MAT ,过滤猜测的、可能出现问题的类,可以看到,创建的这些自定义对象,不多不少正好一百个。

 

 

右键点击类,然后选择 incoming ,这会列出所有的引用关系。

 

 

Path To GC Roots

被JVM 持有的对象,如当前运行的线程对象,被 systemclass loader 加载的对象被称为 GC Roots ,从一个对象到 GC Roots 的引用链被称为 Path to GC Roots , 通过分析 Path to GC Roots 可以找出 JAVA 的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径(这个对象可能内存泄漏)。 再次选择某个引用关系,然后选择菜单“Path To GC Roots ”,即可显示到 GC Roots 的全路径。通常在排查内存泄漏的时候,会选择排除虚弱软等引用。

 

使用这种方式,即可在引用之间进行跳转,方便的找到所需要的信息(这里从对象反推到了线程 king-thread ) , 也可以快速定位到有内存泄漏的问题代码。

 

10、高级功能— OQL

MAT 支持一种类似于 SQL 的查询语言 OQL ( Object Query Language ),这个查询语言 VisualVM 工具也支持。

 

查询A 对象:

select * from ex14.ObjectsMAT$A

查询包含 java 字样的所有字符串:

select * from java.lang.String s wheretoString (s) like ".*java.*"

OQL 有比较多的语法和用法,若想深入了解,可以了解这个网址 http://tech.novosoft-us.com/products/oql_book.htm

到此JVM 利用MAT 工具分析内存泄漏详解结束,下篇在利用MAT工具实战分析内存泄漏,敬请期待。