本文目录导读:

- 案例背景:一次“内存泄漏”引发的线上事故
- 第一步:打开堆文件与基础体检
- 第二步:定位占用大头(Dominator Tree)
- 第三步:对象引用链分析(追赶GC Roots)
- 第四步:深入内部(查看大对象成因)
- 第五步:确认Finalization与GC问题(进阶排查)
- 第六步:对比分析(若有两个堆文件)
- 第七步:形成诊断报告与解决建议
- MAT分析黄金三步
MAT(Memory Analyzer Tool) 是分析Java堆转储(Heap Dump)的核心工具,下面通过一个完整的教学案例,带您从拿到堆文件到定位问题,走一遍完整的分析流程。
案例背景:一次“内存泄漏”引发的线上事故
现象:某电商订单系统在每天下午3点(订单高峰期)频繁触发Full GC,响应时间从50ms飙升到5秒,最终导致服务器OOM(OutOfMemory)宕机。
动作:运维在宕机前抓取了一个 heap.bin 文件(约2GB),现在需要我们通过MAT分析出问题根因。
第一步:打开堆文件与基础体检
- 启动MAT:
File -> Open Heap Dump,选择heap.bin。 - 等待加载:大文件会弹出提示,选择 Leak Suspects Report(泄漏嫌疑报告),这是最常用的入口。
MAT会生成一个概览(Overview),最上方是关键数据:
- Total Heap:总堆内存 2GB。
- Classes:类数量 45,000。
- Instances:对象数量 8,000,000。
- Class Loader:类加载器数量。
一眼看重点:点击右侧工具栏的 “Histogram”(直方图),查看所有类的实例数量和占用空间。
历史图排序列:按 Retained Heap(保留堆内存,即该对象被释放后能回收的内存)降序排列。
此时你可能会看到如下异常数据:
| Class Name | Objects | Shallow Heap | Retained Heap |
|---|---|---|---|
java.util.HashMap$Node[] |
120,000 | 1 MB | 800 MB |
com.example.order.model.OrderInfo |
1,500,000 | 45 MB | 500 MB |
初步判断:HashMap 数组的对象数量虽然不多(12万个),但保留内存高达800MB,说明有大量的对象被这个数组引用,形成了巨大的对象树。
第二步:定位占用大头(Dominator Tree)
关闭直方图,打开 Dominator Tree(支配树)(在 Open Query Browser 中,路径:Java Basics -> Dominator Tree)。
支配树会帮你算清整个堆的依赖关系,直接告诉你谁占用了最多内存。
| 对象名称 | Retained Heap | 占比 |
|---|---|---|
java.util.HashMap @ 0x6c0015e0 |
850 MB | 42% |
java.lang.Thread @ 0x7a105c20 |
400 MB | 20% |
byte[] @ 0x3d0f4a2c |
200 MB | 10% |
最大的对象是一个 HashMap,占用了整个堆的 42%,很明显,肯定是一张业务缓存表无限增长导致泄漏。
第三步:对象引用链分析(追赶GC Roots)
右键点击这个 HashMap @ 0x6c0015e0,选择 Path to GC Roots -> with all references(即排查谁引用了它,阻止其被垃圾回收)。
分析结果树形展开如下(这是最核心的一步):
java.lang.Thread @ 0x7a105c20 'http-nio-8080-exec-12'
└── org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor
└── com.example.order.service.OrderCacheFilter
└── (referenced by static field) 'orderCache' <-- 静态变量引用
└── java.util.HashMap @ 0x6c0015e0 (860 MB)
根因显现:
OrderCacheFilter 类中的 静态字段 orderCache 持有这个HashMap的引用,因为它是 static(静态)的,属于GC Root(根对象),它链接着所有数据,导致整个地图无法被回收。
第四步:深入内部(查看大对象成因)
右键点击这个巨大的HashMap,选择 List Objects -> with outgoing references(查看它引用了什么)。
关键发现:
- 这个HashMap的 Key 是
String(订单号)。 - Value 是
OrderInfo对象(订单详情)。 - 条数为 150万条。
结合业务逻辑猜测:这个 OrderCacheFilter 本意是想把“订单查询结果”缓存起来,代码中没有设置缓存的过期时间(TTL)或容量上限(eviction policy),每时每刻有新订单,Map就会无条件地增加新条目,最终占满堆内存。
第五步:确认Finalization与GC问题(进阶排查)
在 Leak Suspects 报告中,MAT通常会给出两个重点提示:
- “Problem Suspect 1”:占用了大量内存的
HashMap(如上分析)。 - “Problem Suspect 2”:
Finalizer堆积。
很多时候OOM伴随一个现象:堆中有大量 java.util.zip.Inflater 或 java.sql.Statement 对象处于pending finalization(等待终结器)状态,查看 Thread Details,往往能看到 Finalizer 守护线程无法快速处理这些对象,反而是内存泄漏的“帮凶”。
第六步:对比分析(若有两个堆文件)
如果运维在故障前和故障时各抓了一份堆(如 heap_before.bin 和 heap_after.bin),可使用MAT的高级功能:
打开 Window -> Compare,选择文件 heap_after.bin 作为对比对象。
MAT会列出计算出 “顶部的增量”,你会看到 HashMap 增加了 70% 的条目,而其他类保持不变,这进一步压倒了“对象泄漏”,排除了“临时请求”的干扰。
第七步:形成诊断报告与解决建议
最终诊断(MAT分析结论)
- 问题类型:内存泄漏(非内存溢出瞬时表象)。
- 泄漏根:
OrderCacheFilter的静态HashMap orderCache无容量限制。 - 数据特征:150万个订单Key未被清除,深入看发现有大量的
byte[]被缓存的订单JSON序列化数据占用(即Map的Value)。
修复方案(给开发人员的建议)
- 短期修复:将该静态
HashMap替换为 Caffeine 或 Guava Cache,并设置最大容量(如10万)和15分钟过期时间。 - 中期修复:该缓存逻辑本身站不住脚——订单数据变化频繁,高频更新时缓存命中率低,建议改为只在订单状态为“完成”时缓存,其他状态直接查库,并开启定时清理任务。
- 长期修复:使用 Redis 作为分布式缓存,而不是放在JVM本地(避免单机内存压力)。
MAT分析黄金三步
不管什么案例,请务必记住MAT的三步定位法:
- 看历史图(Histogram):找出哪个类的对象数量最多或占内存最大。
- 看支配树(Dominator Tree):找出哪个大对象持有的子树最深。
- 看GC根路径(Path to GC Roots):查看为什么GC无法回收这些对象,重点是寻找静态变量、ThreadLocal、监听器集合导致的误引用。
这个案例涵盖了MAT分析的完整链路,适合作为您分析线上Java内存问题的参考模板,如果堆文件超过2GB,建议在MAT配置中增加 -Xmx 参数(内存分析软件自身内存要大于堆文件),否则无法加载。