2025-06-27
实战设计
0

目录

MAT 简介
MAT 安装配置
MAT 功能介绍
Overview(概要)
Details
Biggest Objects by Retained Size
Actions
Reports
Step By Step
Histogram(柱状图)
引用关系 Incoming/Outgoing References 
MAT中的浅堆与深堆
案例分析
引用变动的影响
使用MAT进行内存泄漏检测
Dominator Tree(支配树视图)
MAT中内存对比
线程(thread_overview)
高级功能—OQL
Leak Suspects
Top Consumers
Path To GC Roots
实战
原因分析
总结

前面讲过,使用 jmap –histojcmd 命令去分析哪些对象占据着我们的堆空间,如果是遇到内存情况比较复杂的情况,就需要导出 dump 文件进行分析了,这个时候我们必须要借助一下工具,本文将以 MAT 开展分析过程。

MAT 简介

MAT 工具是基于 Eclipse 平台开发的,本身是一个 Java 程序,是一款很好的内存分析工具,适用于复杂的内存分析场景。

官网下载地址 https://eclipse.dev/mat/download/

Mac x86_64 版本(下载有点慢,用迅雷): https://mirror.kakao.com/eclipse/mat/1.16.1/rcp/MemoryAnalyzer-1.16.1.20250109-macosx.cocoa.x86_64.dmg

MAT 安装配置

接下来我将以mac版本进行讲解安装的过程。

首先打开下载的 MemoryAnalyzer-1.16.1.20250109-macosx.cocoa.x86_64.dmg,将 MemoryAnalyzer.app 拖动到 Applications 文件夹内,搞定收工,安装完成。

image.png

这个时候我们软件已经安装完成了,接下来调整配置。打开软件的包内容(打开 Applications 文件夹,找到 MemoryAnalyzer.app,右键打开显示包内容),找到 Info.plist 文件进行调整

image.png

从 Info.plist 中找到 array 标签部分,指定启动使用的JDK(当前版本必须17),配置与环境变量的配置一致,工作空间指定当前 app 包内容下的即可,也就是上图中的路径。

xml
<array> <!-- 指定启动使用的JVM --> <string>-vm</string> <string>/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/java</string> <!-- 指定启动的工作空间 --> <string>-data</string> <string>/Applications/MemoryAnalyzer.app/Contents/MacOS/workspace</string> <!-- 原有内容,不动 --> <string>-keyring</string> <string>~/.eclipse_keyring</string> </array>

所以如果你的堆快照比较大的话,则需要给 MAT 本身加大初始内存,这个可以修改安装目录中的 MemoryAnalyzer.ini 文件,根据电脑硬件配置自行设置内存大小

image.png

image.png

MAT 功能介绍

file -> open heap dump 打开文件,默认主页包含 Overview(概要)、Histogram(柱状图)、dominator_tree(支配树视图)、QQL、线程(thread_overview) 这几部分。

image.png

提示

支配树视图 是使用频率最高的功能项,是高效分析 Dump 必看的功能。

分析到大对象后,结合 Path To GC Roots -> Incoming/Outgoing References 查询其引用关系,可立马定位大部分内存泄漏的问题所在。

Overview(概要)

概要是分析demp时打开的默认页面,里面分为5部分信息: Details、 Biggest Objects by Retained Size、Actions、Reports、Step By Step,下面将慢慢讲解

Details

image.png

Biggest Objects by Retained Size

image.png

Actions

  • Histogram 列出每个类所对应的对象个数,以及所占用的内存大小;
  • Dominator Tree 以占用总内存的百分比的方式来列举出所有的实例对象,注意这个地方是直接列举出的对应的对象而不是类,这个视图是用来发现大内存对象的
  • Top Consumers:按照类和包分组的方式展示出占用内存最大的一个对象
  • Duplicate Classes:检测由多个类加载器所加载的类信息(用来查找重复的类)

Reports

  • Leak Suspects:通过MAT自动分析当前内存泄露的主要原因
  • Top Components:Top组件,列出大于总堆1%的组件的报告

Step By Step

  • Component Report:组件报告,分析属于公共根包或类加载器的对象;

Histogram(柱状图)

柱状图视图,可以看到类的实例对象个数,以及所占用的内存大小。此处选中一 ClassName 单击后,通过左上角Inspector可以看到当前类的回收情况,内存地址等

image.png

  • Class Name:当前类的全路径类名
  • Objects:表示当前类所对应的对象数量
  • Shallow Heap:Shallow Size是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的Shallow Size由其成员变量的数量和类型来定,而数组的ShallowSize由数组类型和数组长度来决定,它为数组元素大小的总和;
  • Retained Heap:Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C,C就是间接引用) ,并且排除被GC Roots直接或者间接引用的对象;

左下侧tab内的 Statics、Attributes、Classhierarchy、Value 则分别表示当前类的静态变量,属性,当前类的层次结构图,以及当前类所对应的值Value;

注意

当前 Histogram 的列属性:ClassName,Objects,ShallowHeap,RetainedHeap 这几个列属性都是有提供一个隐藏输入框,通过该输入框可以进行相关类的检索,比如:在ClassName 下输入一个 com.baidu , 那么则获取到所有包路径为 com.baidu 的类信息。

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

通过 Histogram 列出每个类所对应的对象个数,

image.png

image.png

image.png

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

image.png

image.png

引用关系 Incoming/Outgoing References 

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

但是MAT有一个专业的概念,这个可以显示对象的引入和对象的引出。在 Eclipse MAT 中,当右键单击任何对象时,将看到下拉菜单。如果选择“ListObjects”菜单项,则会注意到两个选项:

  • with incoming references 对象的引入
  • with outgoing references 对象的引出

案例解释理解

image.png

代码中对象和引用关系如下: 对象 A 和对象 B 持有对象 C 的引用 对象 C 持有对象 D 和对象 E 的引用

image.png

我们具体分析对象 C 的 Incoming references 和 Outgoing references 。 1、程序跑起来

image.png

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

image.png

image.png

image.png

image.png

image.png

image.png

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

我们再来分析下outgoing reference

image.png

image.png

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

MAT中的浅堆与深堆

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

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

需要说明一下:JAVA对象大小=对象头+实例数据+对齐填充 非数组类型的对象的shallow heap shallow_size=对象头+各成员变量大小之和+对齐填充 其中,各成员变量大小之和就是实例数据,如果存在继承的情况,需要包括父类成员变量 数组类型的对象的shallow size shallow size=对象头+类型变量大小数组长度+对齐填充,如果是引用类型,则是四字节或者八字节(64位系统), 如果是boolean类型,则是一个字节 注意:这里 类型变量大小数组长度 就是实例数据,强调是变量不是对象本身

image.png

案例分析

image.png

对象 A 持有对象 B 和 C 的引用。 对象 B 持有对象 D 和 E 的引用。 对象 C 持有对象 F 和 G 的引用。

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

引用变动的影响

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

image.png

在这种情况下,对象 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进行内存泄漏检测

如果问题特别突出,则可以通过 Find Leaks 菜单快速找出问题。 运行以下代码, 我们开始跑程序:

image.png

image.png

image.png

image.png

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

image.png

image.png

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

Dominator Tree(支配树视图)

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

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

比如下图,Thread 这个对象占据了99.68%的堆大小,所以基本就可以断定,当前项目之所以会down机的主要原因就是其溢出所导致的问题; image.png

image.png

image.png

从上图层层分解,我们也知道,原来是king-thread的深堆和浅堆比例很多(深堆比浅堆多很多、一般经验都是找那些浅堆比较小,同时深堆比较大的对象) 1、一个浅堆非常小的king-thread持有了一个非常大的深堆 2、这个关系来源于一个HashMap 3、这个map中有对象A,同时A中引用了B,B中引用了C 4、最后找到C中里面有一个ArrayList引用了一个大数据的数组。 经过分析,内存的泄漏点就在此。一个线程长期持有了200个这样的数组,有可能导致内存泄漏。

MAT中内存对比

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

image.png

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

image.png

image.png

image.png

image.png

image.png

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

线程(thread_overview)

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

image.png

image.png

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

image.png

image.png

image.png

高级功能—OQL

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

image.png

查询 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

Leak Suspects

具备自动检测内存泄漏功能,通过MAT自动分析当前内存泄露并罗列可能存在内存泄漏的问题点。

使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。

下图中 Leak Suspects 视图展现了两个线程支配了绝大部分内存。

image.png

可以看到,当前MAT所给出内存泄露的主要原因是:当前实例java.util.concurrent.ConcurrentHashMap被加载自system class loader,共占用了 98.92%的堆内存,这个实例被引用自org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl并且这个CacheObjectBinaryProcessorImpl这个对象是加载自LaunchedURLClassLoader这个类加载器;

并且还给出了所对应的主要关键词是:

java
java.util.concurrent.ConcurrentHashMap$Node[] java.util.concurrent.ConcurrentHashMap org.springframework.boot.loader.LaunchedURLClassLoader @ 0x6000a6860

基本上可以说是很详细了,一语中的,如果想要查看明细,可以直接点击detail,里面有更详细的说明。

Top Consumers

最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。

使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。

Path To GC Roots

使用入口:目标域右键 → List objects → with outgoing references/with incoming references.

被JVM持有的对象,如当前运行的线程对象,被systemclass loader加载的对象被称为GC Roots, 从一个对象到GC Roots的引用链被称为Path to GC Roots, 通过分析Path to GC Roots可以找出JAVA的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径(这个对象可能内存泄漏)。

再次选择某个引用关系,然后选择菜单“Path To GC Roots”,即可显示到 GC Roots 的全路径。通常在排查内存泄漏的时候,会选择排除虚弱软等引用。

在对象引用图中查看某个特定对象的所有引用关系(提供对象对其他对象或基本类型的引用关系,以及被外部其他对象的引用关系)。通过任一对象的直接引用及间接引用详情(主要是属性值及内存占用),提供完善的依赖链路详情。

image.png

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

image.png

实战

JVM 启动参数

sh
-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/Desktop/dump/heapdump.hprof -XX:-UseCompressedClassPointers -XX:-UseCompressedOops

image.png

开始分析

image.png

image.png

image.png

在这里我们就可以很明显地查看到是 ThreadLocal 这块的代码出现了问题。

原因分析

ThreadLocal是基于ThreadLocalMap实现的,这个Map的Entry继承了WeakReference,而Entry对象中的key使用了WeakReference封装,也就是说Entry中的key是一个弱引用类型,而弱引用类型只能存活在下次GC之前。

image.png

image.png

image.png

当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。 当发生一次垃圾回收,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(肯定不会结束),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。如下图:

image.png

只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收(但是这种情况很难)。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

image.png

总结

可以看到,上手 MAT 工具是有一定门槛的,除了其操作模式,还需要对我们前面介绍的理论知识有深入的理解,比如 GC Roots、各种引用级别等。 如果不能通过大对象发现问题,则需要对快照进行深入分析。使用柱状图和支配树视图,配合引入引出和各种排序,能够对内存的使用进行整体的摸底。由于我们能够看到内存中的具体数据,排查一些异常数据就容易得多。 上面这些问题通过分析业务代码,也不难发现其关联性。问题如果非常隐蔽,则需要使用 OQL 等语言,对问题一一排查、确认。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!