在排查 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
找到:
HistogramHistogram 可以简单理解成:
按照 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 没有把它回收?