玩转MAT—dump与动态分析

前言

我们使用jmap-histo这种命令去分析哪些对象占据着我们的堆空间。但是那是比较容易分析的问题。如果遇到的是内存情况比较复杂的情况,基础命令是不容易分析问题的。这时,我们需要借助工具。今天介绍的工具MAT,既可以使用dump日志分析内存问题,也可以在程序运行期间,获取程序当时运行的快照进行分析。

内存分析工具

JDK自带工具VisualVM

案例

分析代码

本程序可以大概看一下,首先先启动一个线程池。这个线程池的其实容量与最大容量设置死,也就是说最多可同时容纳开启50个线程执行任务。并且队列使用的无界队列。

仔细分析代码可以知道,我们每0.1s给线程池丢100个任务,而线程池没3s才搞定一个任务。因此这些未执行的任务的对象无法回而导致OOM。

-XX:+HeapDumpOnOutOfMemoryError//此参数添加后,可以在程序OOM后自动生成dump日志。

使用VisualVM

VisualVM目录在我们的jdk/bin下。

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

MAT简介

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

 

概要

柱状图

MAT中的Incoming/Outgoing References

在柱状图中,我们看到,其实它显示的东西跟 jmap –histo 非常相似的,也就是类、实例数量、空间大小。

但是 MAT 有一个专业的概念,这个可以显示对象的引入和对象的引出。

在 Eclipse MAT 中,当右键单击任何对象时,将看到下拉菜单。如果选择“ListObjects”菜单项,则会注意到两个选项:

  1. Ø with incoming references 对象的引入
  2. Ø with outgoing references 对象的引出

案例

我们说两个类直接的关系除了继承之外,还可以是组合。

我们根据上述代码可以画出各类的组合关系图:

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

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

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

运行程序

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

可以从上面的图中分析,看到incoming references代表该类对象被哪些类所持有。相反的,outgoing reference代表本类所持有哪些类的对象。

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

MAT中的浅堆和深堆

浅堆(shallow heap

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

深堆(Retained heap

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

Java对象内存大小

JAVA 对象大小=对象头+实例数据+对齐填充

非数组类型

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

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

数组类型

深堆(Retained _size)=对象头+类型变量大小*数组长度+对齐填充,如果是引用类型,则是四字节或者八字节(64 位系统),如果是 boolean 类型,则是一个字节 。

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

 

案例分析

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

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

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

一句话,浅堆就是自身对象大小,那么深堆就是根可达分析自己后面的引用链对象总大小+自己的大小呢?我们接着看。往往深堆更能体现出一个对象的回收价值。

引用变动的影响

在下面的示例中,让对象 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.

总结:因此我们说深堆大小并不能直接从引用链的角度来分析,而是要站在垃圾回收的根可达分析算法的角度,看这个对象会收的后真正可以释放的内存大小。我们分析问题时,往往去那些深堆很大,尤其那些浅堆比较小,但深堆比较大的对象。它们极有可能是有问题的对象。

使用 MAT 进行内存泄漏检测

案例

仔细分析代码,我们可以发现A引出B引出C。king-thread线程中,包含一个map,map又引出了A(包含了A的实例a)。同时Demo1与Demo2互相引用。总结为下图关系:

运行程序并使用MAT调试

当我们产生问题时,MAT会帮助我们进行问题猜测。这个程序的泄漏很明显,就是线程无法结束,导致map无法销毁,导致深堆过大的内存泄漏。

这里一个名称叫做 king-thread 的线程,持有了超过 99% 的对象,数据被一个 HashMap 所持有。

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

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

支配树视图

支配树列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具可以帮助我们定位对象间的引用情况,以及垃圾回收时的引用依赖关系。支配树视图对数据进行了归类,体现了对象之间的依赖关系。我们通常会根据“深堆”进行倒序排序,可以很容易的看到占用内存比较高的几个对象,点击前面的箭头,即可一层层展开支配关系(依次找深堆明显比浅堆大的对象)。

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

象)

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

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

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

4、 最后找到 C 中里面有一个 ArrayList 引用了一个大数据的数组。经过分析,内存的泄漏点就在此。线程中的map长期持有100个超大的ArrayList ,有可能导致内存泄漏。

MAT 中内存对比

我们对于堆的快照,其实是一个“瞬时态”,有时候仅仅分析这个瞬时状态,并不一定能确定问题,这就需要对两个或者多个快照进行对比,来确定一个增长趋势。我们导出两份 dump 日志,分别是上个例子中循环次数分别是 10 和 100 的两份日志。

线程视图

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

我们通过展开的层层定位也找到了主要占据内存的是这个list。

还有另外一段是陷入无限循环,这个是相互引用导致的(进行问题排查不用被这种情况给误导了,这样的情况一般不会有问题---可达性分析算法的解决了相互引用的问题)。具体可以结合上面给出的代码分析图。

柱状图视图

柱状图视图,可以看到除了对象的大小,还有类的实例个数。结合 MAT 提供的不同显示方式,往往能够直接定位问题。可以看到,创建的这些自定义对象,不多不少正好一百个。

可以看到该类的引入的具体对象

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 的全路径。通常在排查内存泄漏的时候,会选择排除虚弱软等引用。

我们通过刚才柱状图视图获得了对象的列表。

我们就可以得到清晰地该对象的引入链,一直到其对应的GCRoots。

高级功能—OQL

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

查询 A 对象:

select * from ex14.ObjectsMAT$A

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

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

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

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/110873144