在排查 TongWeb、Tomcat、Spring Boot 等 Java 服务的内存问题时,经常会遇到这样的情况:
JVM 内存越来越高
↓
Full GC 越来越频繁
↓
GC 后内存仍然降不下来
↓
最终 OutOfMemoryError
这时候,仅仅通过 top、jstat 查看 JVM 内存,只能知道"内存有问题",却很难知道到底是什么对象占用了内存。
这时候就可以使用 Eclipse Memory Analyzer(MAT)。
简单来说:
- MAT 就是一款专门用来分析 Java Heap Dump 的工具,可以帮助我们找到占内存最多的对象,以及这些对象为什么一直没有被 GC 回收。
- 最主要的是完全免费,不需要进行pojie等操作。
MAT 主要用来解决什么问题?
MAT 最常见的使用场景主要有以下几种:
1. Java 服务频繁 OOM
例如日志出现:
java.lang.OutOfMemoryError: Java heap space
可以在 JVM OOM 后生成的 Heap Dump 中寻找问题。
2. JVM 内存持续上涨
例如:
启动:2G
运行一天:3G
运行三天:5G
运行一周:7G
而且 Full GC 之后内存仍然很高,就需要重点怀疑对象没有正常释放。
3. Full GC 频繁
如果发现 JVM 不断进行 Full GC:
Full GC
Full GC
Full GC
但是回收效果越来越差,也可以通过 Heap Dump 分析到底是什么对象占用了堆内存。
4. 怀疑内存泄漏
例如:
缓存没有清理
ThreadLocal 使用不当
静态 Map 持有大量对象
Session 保存大量数据
Web 应用重复部署后 ClassLoader 没有释放
这些问题都可以通过 MAT 进一步分析。
MAT 的整体排查思路
第一次使用 MAT,不需要把所有功能都学会。
记住下面这条路线就够了:

第一步:找到服务器上的 Java 进程
首先登录服务器。
执行:
jps -l
例如:
[root@server ~]# jps -l
12345 org.apache.catalina.startup.Bootstrap
13579 sun.tools.jps.Jps
这里:
12345
就是我们需要分析的 Java 进程 PID。

第二步:检查磁盘空间
Heap Dump 可能非常大。
例如:
JVM 最大堆:8G
生成出来的 .hprof 文件可能达到几个 GB。
所以生成之前建议先执行:
df -h
确认目标磁盘空间足够。
例如:
Filesystem Size Used Avail Use%
/dev/sda3 100G 55G 45G 56%

第三步:生成 Heap Dump
现在假设 Java PID 是:
12345
推荐使用 jcmd:
jcmd 12345 GC.heap_dump /data/dump/tongweb.hprof
也可以使用 jmap:
jmap -dump:format=b,file=/data/dump/tongweb.hprof 12345
生成成功后会得到:
tongweb.hprof
这个文件就是后面 MAT 分析的对象。

确认文件是否生成成功
执行:
ls -lh /data/dump/tongweb.hprof
例如:
-rw------- 1 root root 4.2G Sep 3 13:20 tongweb.hprof
这里重点看文件大小。
如果发现:
4.2G
说明这个文件比较大,后续下载和 MAT 分析都需要一定时间。

第四步:把 Heap Dump 下载到本地
服务器上的文件一般不建议直接在服务器上分析。
通常是:
服务器
↓
生成 hprof
↓
下载
↓
自己的电脑
↓
MAT 分析
例如 Mac/Linux 可以使用:
scp root@192.168.1.100:/data/dump/tongweb.hprof .
如果 SSH 使用其他端口:
scp -P 2222 root@192.168.1.100:/data/dump/tongweb.hprof .
也可以使用 Xftp、WinSCP、SFTP 等工具传输。

第五步:使用 MAT 打开 Heap Dump
打开 Eclipse Memory Analyzer。
你现在看到的这个界面就是 MAT 的主界面。

然后点击:
File
↓
Open Heap Dump
选择:
tongweb.hprof
也可以直接点击:
Open a Heap Dump
然后选择 .hprof 文件。

打开之后先看 Overview
Heap Dump 加载完成后,MAT 会展示整体信息。
这里可以先看看:
对象数量
Class 数量
Class Loader
堆内存情况
不用一开始就研究所有数据。
先建立一个概念:
这个 Heap Dump 记录的到底是一个什么样的 JVM 内存现场。

第一项重点分析:Leak Suspects
MAT 通常会提供:
Leak Suspects
可以理解成:
MAT 根据当前 Heap Dump 自动找出来的可疑内存占用点。
例如:
Problem Suspect 1
One instance of xxx
retains a large amount of memory
这时候可以继续点击进去查看具体对象和引用关系。

不过要注意:
Leak Suspects 标记出来的对象不一定就是内存泄漏。
比如一个正常的大缓存,本身就可能占用几个 GB。
所以还需要继续分析。
第二项重点分析:Histogram
找到:
Histogram
Histogram 可以简单理解成:
按照 Java 类统计对象数量和内存占用。
例如:
Class Name Objects Shallow Heap
-------------------------------------------------------
java.lang.String 3000000 120 MB
byte[] 1000000 800 MB
java.util.HashMap$Node 900000 30 MB
com.xxx.User 500000 40 MB
这里重点观察两个问题:
对象数量是不是异常?
例如:
User:500万
Order:300万
String:1000万
如果业务实际上只有几十万用户,这就值得调查。
哪些对象占用内存最多?
尤其关注:
byte[]
char[]
String
HashMap
ConcurrentHashMap
业务自己的对象

第三项重点分析:Dominator Tree
如果想进一步找:
到底是谁"占住"了大量内存?
可以打开:
Dominator Tree
重点关注:
Retained Heap
例如:
com.xxx.Cache 3.2 GB
HashMap 2.8 GB
com.xxx.UserManager 1.5 GB
byte[] 900 MB
这时候:
com.xxx.Cache
就值得重点检查。
因为它自己可能只占几十 MB,但它引用了大量其他对象,最终导致:
Retained Heap = 3.2 GB
这也是 MAT 排查内存问题时非常重要的一个指标。

最后一个关键分析:Path to GC Roots
假设我们发现:
com.xxx.User
占用了大量内存。
接下来最关键的问题是:
为什么这些 User 一直没有被 GC 回收?
可以右键对象,找到:
Path to GC Roots
然后查看引用链。
例如:
GC Root
↓
Thread
↓
ThreadLocalMap
↓
ThreadLocal
↓
User
或者:
GC Root
↓
Static Field
↓
Cache
↓
HashMap
↓
User
这时候就开始接近真正的问题了。


exclude all phantom/weak/soft etc. references👉【排查泄漏首选】只保留强引用,屏蔽缓存类弱引用干扰。include all references:全部引用都展示(会出来大量 WeakHashMap 等弱引用,信息很乱)。
实际排查时主要关注哪些对象?
如果是生产环境 Java 应用,我一般会重点关注这些:
| 类型 | 重点检查 |
|---|---|
HashMap |
是否无限增长 |
ConcurrentHashMap |
是否存在无上限缓存 |
String |
数量是否异常 |
byte[] |
是否存在大量数据、文件、请求内容 |
Thread |
是否存在异常线程 |
ThreadLocal |
是否长期持有业务对象 |
Session |
是否保存了大量数据 |
ClassLoader |
是否存在重复部署导致无法释放 |
| 业务对象 | 是否数量异常、长期存活 |
特别是看到:
Retained Heap 很大
不要马上判断是泄漏。
应该继续问:
谁引用它?
为什么引用?
这个对象正常情况下应该存在多久?
有没有清理机制?
一个简单的实际案例
假设客户反馈:
TongWeb 运行几天以后内存越来越高,最后 OOM。
我们可以按照下面的方式排查:

最终可能发现:
某个 static ConcurrentHashMap
↓
不断 put 数据
↓
没有过期机制
↓
对象越来越多
↓
Retained Heap 持续增长
↓
Full GC 无法回收
↓
最终 OOM
这才是一次比较完整的内存问题排查。
还有一个非常实用的方法:对比多个 Heap Dump
如果问题是:
内存随着运行时间不断上涨
不要只生成一个 Heap Dump。
可以在不同时间分别生成:
dump-01.hprof
dump-02.hprof
dump-03.hprof
例如:
第一次:JVM 使用 3G
第二次:JVM 使用 5G
第三次:JVM 使用 7G
然后分别使用 MAT 分析。
重点比较:
哪些对象越来越多?
哪些对象 Retained Heap 不断增加?
哪些对象始终没有被释放?
这种方式往往比只分析一次 Heap Dump 更容易发现真正的内存泄漏。
小结一下
如果刚开始接触 MAT,不需要一次把所有功能都学会。
先记住这几个:
Heap Dump
↓
Leak Suspects
↓
Histogram
↓
Dominator Tree
↓
Path to GC Roots
分别解决:
Heap Dump
→ 保存 JVM 某一时刻的内存现场
Leak Suspects
→ MAT 帮你找可疑点
Histogram
→ 看什么对象最多
Dominator Tree
→ 看谁占住了最多内存
Path to GC Roots
→ 看为什么这些对象一直没有被回收
最终目的不是"看懂 MAT 里的所有数据",而是回答三个问题:
谁占用了内存?
为什么占这么多?
为什么 GC 没有把它回收?