MAT分析案例

wen java案例 1

本文目录导读:

MAT分析案例

  1. 案例背景:一次“内存泄漏”引发的线上事故
  2. 第一步:打开堆文件与基础体检
  3. 第二步:定位占用大头(Dominator Tree)
  4. 第三步:对象引用链分析(追赶GC Roots)
  5. 第四步:深入内部(查看大对象成因)
  6. 第五步:确认Finalization与GC问题(进阶排查)
  7. 第六步:对比分析(若有两个堆文件)
  8. 第七步:形成诊断报告与解决建议
  9. MAT分析黄金三步

MAT(Memory Analyzer Tool) 是分析Java堆转储(Heap Dump)的核心工具,下面通过一个完整的教学案例,带您从拿到堆文件到定位问题,走一遍完整的分析流程。


案例背景:一次“内存泄漏”引发的线上事故

现象:某电商订单系统在每天下午3点(订单高峰期)频繁触发Full GC,响应时间从50ms飙升到5秒,最终导致服务器OOM(OutOfMemory)宕机。

动作:运维在宕机前抓取了一个 heap.bin 文件(约2GB),现在需要我们通过MAT分析出问题根因。


第一步:打开堆文件与基础体检

  1. 启动MATFile -> Open Heap Dump,选择 heap.bin
  2. 等待加载:大文件会弹出提示,选择 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的 KeyString(订单号)。
  • ValueOrderInfo 对象(订单详情)。
  • 条数为 150万条

结合业务逻辑猜测:这个 OrderCacheFilter 本意是想把“订单查询结果”缓存起来,代码中没有设置缓存的过期时间(TTL)容量上限(eviction policy),每时每刻有新订单,Map就会无条件地增加新条目,最终占满堆内存。


第五步:确认Finalization与GC问题(进阶排查)

Leak Suspects 报告中,MAT通常会给出两个重点提示:

  1. “Problem Suspect 1”:占用了大量内存的 HashMap(如上分析)。
  2. “Problem Suspect 2”Finalizer 堆积

很多时候OOM伴随一个现象:堆中有大量 java.util.zip.Inflaterjava.sql.Statement 对象处于pending finalization(等待终结器)状态,查看 Thread Details,往往能看到 Finalizer 守护线程无法快速处理这些对象,反而是内存泄漏的“帮凶”。


第六步:对比分析(若有两个堆文件)

如果运维在故障前故障时各抓了一份堆(如 heap_before.binheap_after.bin),可使用MAT的高级功能:
打开 Window -> Compare,选择文件 heap_after.bin 作为对比对象。
MAT会列出计算出 “顶部的增量”,你会看到 HashMap 增加了 70% 的条目,而其他类保持不变,这进一步压倒了“对象泄漏”,排除了“临时请求”的干扰。


第七步:形成诊断报告与解决建议

最终诊断(MAT分析结论)

  • 问题类型内存泄漏(非内存溢出瞬时表象)。
  • 泄漏根OrderCacheFilter 的静态 HashMap orderCache 无容量限制。
  • 数据特征:150万个订单Key未被清除,深入看发现有大量的 byte[] 被缓存的订单JSON序列化数据占用(即Map的Value)。

修复方案(给开发人员的建议)

  1. 短期修复:将该静态 HashMap 替换为 CaffeineGuava Cache,并设置最大容量(如10万)和15分钟过期时间。
  2. 中期修复:该缓存逻辑本身站不住脚——订单数据变化频繁,高频更新时缓存命中率低,建议改为只在订单状态为“完成”时缓存,其他状态直接查库,并开启定时清理任务
  3. 长期修复:使用 Redis 作为分布式缓存,而不是放在JVM本地(避免单机内存压力)。

MAT分析黄金三步

不管什么案例,请务必记住MAT的三步定位法

  1. 看历史图(Histogram):找出哪个类的对象数量最多占内存最大
  2. 看支配树(Dominator Tree):找出哪个大对象持有的子树最深。
  3. 看GC根路径(Path to GC Roots):查看为什么GC无法回收这些对象,重点是寻找静态变量ThreadLocal监听器集合导致的误引用。

这个案例涵盖了MAT分析的完整链路,适合作为您分析线上Java内存问题的参考模板,如果堆文件超过2GB,建议在MAT配置中增加 -Xmx 参数(内存分析软件自身内存要大于堆文件),否则无法加载。

抱歉,评论功能暂时关闭!