MAT中“Dominator Tree”默认按不可回收内存排序,需重点关注char[]、byte[]、HashMap$Node、String等基础容器的异常实例数或大小,其背后常被静态ConcurrentHashMap或未清理ThreadLocal长期持有;勾选“Exclude weak/soft/phantom references”,用“Merge Shortest Paths to GC Roots”定位强引用链;大量IOUtils或TextNode提示JSON响应未释放;Histogram查java.lang.Class泄漏更有效,尤其Spring热部署场景;Leak Suspects报告仅作参考,需结合缓存配置与日志MDC检查;务必确认dump为OOM瞬间生成且文件完整,MAT内存不足时需调大-Xmx。

怎么看MAT里“Dominator Tree”里的可疑对象
OOM后生成的堆转储(heap dump)文件,用MAT打开默认显示char[]、byte[]、HashMap$Node、String这类基础容器——不是因为你写了多少new String(),而是它们背后被谁长期持有。
常见错误现象:java.lang.OutOfMemoryError: Java heap space发生后,MAT里看到几百MB的char[],但找不到业务类直接引用它;点开“Path to GC Roots”发现最终停在某个静态ConcurrentHashMap或未关闭的ThreadLocal变量上。
- 务必勾选“Exclude weak/soft/phantom references”,否则大量缓存类(如
SoftReference包装的图片)会干扰判断 - 右键某个可疑对象 → “Merge Shortest Paths to GC Roots”比默认的“with all references”更干净,能快速定位强引用链
- 如果看到大量重复的
org.apache.commons.io.IOUtils或com.fasterxml.jackson.databind.node.TextNode,大概率是JSON解析后没释放流或缓存了完整响应体
为什么用“Histogram”查java.lang.Class泄漏比查对象更有效
很多OOM其实不是堆里塞满了业务对象,而是类加载器泄漏(ClassLoader leak),导致老年代堆满却GC不掉——这时候java.lang.Class实例数会持续上涨,每个只占几KB,但加起来压垮元空间或触发Full GC失败。
使用场景:Spring Boot热部署、OSGi插件、自定义URLClassLoader反复加载jar时最典型;MAT里Histogram视图按类名统计实例数,比肉眼扫
立即学习“Java免费学习笔记(深入)”;
- 在
Histogram中输入java.lang.Class回车,看“Retained Heap”列是否随重启次数线性增长 - 右键某行 → “List objects” → “with incoming references”,再点一个
Class实例 → “Path to GC Roots”,重点看是不是被某个静态Map<String, Class>或线程上下文类加载器(Thread.currentThread().getContextClassLoader())持有着 - 注意区分JDK自带类(如
java.util.ArrayList)和你自己的com.xxx.service.UserServiceImpl$$EnhancerBySpringCGLIB$$——后者才是泄漏源
Leak Suspects报告不准?别全信,但要看它标红的引用链
MAT自动运行的Leak Suspects报告只是启发式扫描,它把“持有大量对象且本身不被GC Roots直接引用”的集合类(如ArrayList、LinkedHashMap)标为嫌疑,但90%的情况是你的缓存逻辑写得太糙,不是真正的泄漏。
性能影响:这个报告本身不耗时,但如果你在1GB以上的dump上反复点“Run Report”,MAT会重新计算支配树,卡住几分钟。
- 先看报告顶部的“Details”里写的“Problem Suspect 1”,它给出的“Accumulated Objects”数量是否合理(比如一个订单系统缓存了50万
Order对象,但实际日活才2千,就明显不对) - 点开“See stacktrace”——如果调用栈里全是
org.springframework.cache.interceptor.CacheInterceptor或com.google.common.cache.LocalCache,优先检查缓存配置的maximumSize和expireAfterWrite - 如果报告里提到
java.util.logging.Logger或org.slf4j.impl.Log4jLoggerAdapter,往往是日志框架绑定的MDC没清理,或者Logger被静态Map强引用
分析前必须确认dump文件是哪个阶段的
同一个OOM可能对应多个dump:JVM参数-XX:+HeapDumpOnOutOfMemoryError生成的是抛出OutOfMemoryError瞬间的堆快照,而用jmap -dump:format=b,file=heap.hprof <pid>手动抓的可能是GC刚结束、对象已部分回收的状态——两者看到的主导对象可能完全不同。
容易踩的坑:用线上机器jmap抓dump时没加-F强制参数,结果返回“Unable to open socket file”,或者抓到一半JVM挂了;又或者开发机用VisualVM连远程服务,网络抖动导致dump文件损坏,MAT打开报错Invalid header: expected 0x4846524F46464245。
- 检查dump文件大小是否合理:Java 8下,一个正常运行的2G堆,OOM dump通常在1.2–1.8G之间;如果只有200MB,大概率是CMS GC失败后只dump了年轻代
- 用
file heap.hprof命令看文件头,确认是“Java Heap Dump Binary File”而非“data”或“empty” - 如果MAT提示“Parsing heap dump failed with java.lang.OutOfMemoryError: Java heap space”,不是dump坏了,是你给MAT分配的内存不够,在
MemoryAnalyzer.ini里改-Xmx4g再试
真正难的不是找到那个大数组,而是判断它该不该存在、谁该负责清理、清理时机是否被其他同步逻辑阻塞——这些没法靠工具标红,得翻代码里那几行cache.put(key, value)和cache.remove(key)中间夹着什么。










