简介:MAT(Memory Analyzer Tool)是Eclipse基金会推出的Java堆内存分析工具,专为诊断内存泄漏、内存占用过高及对象生命周期问题而设计,适合需要排查JVM内存异常的Java开发与运维人员。本资源围绕其核心能力展开,即解析JVM生成的hprof标准内存剖析文件,呈现对象分配、存活状态与引用关系,并覆盖支配树、大型对象集、泄漏嫌疑人报告、Shallow Heap与Retained Heap对比、快照对比分析、OQL自定义查询等实用视图,帮助读者逐步定位问题对象与类。压缩包共2000个文件,以html帮助文档、png与gif图示、jar依赖包为主,辅以xml、dita、css、properties等配置与说明文件,整体约75.44MB,结构完整便于离线查阅。目前已有6613人学习下载,可作为内存分析入门与实战排查的参考工具包。
1. 线上 OOM 之后,那个 2GB 的 hprof 到底该怎么读
凌晨两点收到告警,某服务节点堆内存打满,进程被系统干掉,运维丢过来一个 2.3GB 的java_pid12345.hprof。用jhat打开转圈转到天亮,用文本编辑器打开全是乱码,jmap -histo只能看到类级别的统计,根本定位不到是谁把对象攥着不放。这种场景下,MAT(Memory Analyzer Tool)就是那个能把黑匣子拆开给你看的工具。它专门读 hprof 堆转储文件,核心能力是算对象引用链、找支配树、定位内存泄漏嫌疑对象。适合两类人:一类是线上服务出现 OOM 或内存持续上涨、需要事后复盘的后端工程师;另一类是本地压测时想搞清楚对象分配去向的开发者。这篇不聊虚的,从拿到 hprof 到跑出泄漏报告,把参数、命令和翻车点一次讲透。
2. 先搞懂 MAT 读 hprof 的原理:支配树与引用链
2.1 hprof 文件里到底存了什么
hprof 是 JVM 在某个时刻的堆快照,本质是一份二进制转储。它记录了几类关键信息:所有存活对象的类名、实例字段值、对象之间的引用关系、以及每个对象的浅大小(Shallow Size)。注意是「存活对象」——GC 之后仍然可达的对象才会被 dump,所以 hprof 反映的是 dump 那一刻的内存实况,不是历史分配总量。
dump 的触发方式常见有三种:jmap -dump:format=b,file=xxx.hprof <pid>手动触发;启动参数加-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump让 JVM 在 OOM 时自动落盘;还有jcmd <pid> GC.heap_dump /path/xxx.hprof。生产环境我一般强制开第二种,因为 OOM 是偶发的,等你手动去 dump,进程可能已经被重启了。
一个容易忽略的点:hprof 里对象的大小分「浅大小」和「保留大小」。浅大小只算对象自身占的字节,不含它引用的其他对象;保留大小(Retained Size)才是这个对象被回收后能释放的总内存。MAT 的核心价值就在于算保留大小和支配关系,jmap -histo给不了你这个。
2.2 支配树为什么能一眼看出泄漏点
MAT 最核心的算法是支配树(Dominator Tree)。定义很直白:如果从 GC Roots 到对象 B 的所有路径都必经对象 A,那 A 就支配 B。在支配树里,父节点的保留大小等于它自身加上所有被它支配的子节点。
这带来一个反直觉但极有用的结论:一个HashMap实例本身可能只有几十字节,但如果它支配着几万个 Entry,它的保留大小就是几十 MB。你在支配树里按保留大小降序排,排最前面的那个对象,基本就是内存大户。这比在引用链里一层层扒快得多。
MAT 还提供「Leak Suspects」自动报告,它会跑一套启发式规则,把可疑的聚集对象标出来。但别全信它——自动报告有时会把正常的缓存池误报成泄漏,真正的判断还得靠你手动看支配树和引用链。
2.3 引用类型对可达性的影响
Java 有四种引用:强、软、弱、虚。MAT 在算可达性时,默认把软引用、弱引用、虚引用当作「可被回收」处理,也就是说这些引用指向的对象在支配树里可能被当作不可达。这个默认行为很关键:如果你怀疑泄漏对象是被软引用持有的,得在 MAT 的偏好设置里调整引用处理策略,否则它会「假装」这些对象不存在,你就找不到真正的持有者。
常见做法是:先用默认配置跑一遍 Leak Suspects,如果没找到明显大户,再去Window -> Preferences -> Memory Analyzer -> References里把软引用、弱引用都设为「强可达」再重新分析。这一步能救回不少「明明内存涨了却找不到对象」的案子。
3. 从 hprof 到泄漏报告:MAT 实操全流程
3.1 环境准备与内存参数
MAT 本身是个 Java 应用,读大文件时它自己也要吃内存。默认配置下打开 2GB 以上的 hprof 大概率直接 OOM,报Java heap space。所以第一步是改它的启动内存。
找到 MAT 安装目录下的MemoryAnalyzer.ini,改-Xmx:
# MemoryAnalyzer.ini # 分析 2GB 左右的 hprof,建议给 MAT 自身 4GB 以上堆 -Xmx6144m # 大文件解析时避免频繁 Full GC -XX:+UseG1GC逻辑说明:MAT 解析 hprof 时会把索引和对象图加载进自己的堆,经验值是 hprof 大小的 1.5 到 2 倍。一个 2GB 的 dump,MAT 至少需要 4GB 堆才稳。参数改完重启 MAT 生效。如果机器内存不够,可以用 MAT 的「Parse Heap Dump」对话框里勾选「Keep unreachable objects」的反选项,减少索引体积,但会丢失部分信息。
提示:MAT 有 32 位和 64 位版本,分析大文件必须用 64 位,32 位版本堆上限卡死在 4GB 以下,改 ini 也没用。
3.2 打开 hprof 并生成概览
启动 MAT,File -> Open Heap Dump,选中 hprof 文件。第一次打开会弹「Parsing Heap Dump」向导,几个选项值得说:
- Keep unreachable objects:默认不勾。勾了会把不可达对象也纳入分析,文件更大、更慢,一般排查泄漏不需要。
- Create index files:建议勾,生成
.index文件后下次打开快很多。 - Approximate size:如果只想快速看个大概,可以选近似模式,牺牲精度换速度。
解析完成后进入 Overview 页,这里有几个关键入口:Leak Suspects(自动泄漏报告)、Top Consumers(按类/包/类加载器排的大户)、Histogram(类直方图)。我一般的顺序是:先看 Top Consumers 找大类,再进 Dominator Tree 找具体实例,最后用 Path to GC Roots 确认持有链。
3.3 用 OQL 精确定位可疑对象
Histogram 只能看到「某类有多少个实例、共占多少」,但定位泄漏往往需要按条件筛。MAT 内置 OQL(Object Query Language),语法类似 SQL,能直接查对象。
比如怀疑某个自定义缓存类com.example.cache.LocalCache实例过多,可以这样查:
-- 查询 LocalCache 的所有实例,按保留大小降序 SELECT * FROM com.example.cache.LocalCache再比如想找出所有 size 超过 10000 的集合:
-- 筛选 entry 数量异常大的 HashMap SELECT * FROM java.util.HashMap h WHERE h.size > 10000逻辑说明:OQL 的SELECT * FROM <类全名>会返回该类所有实例,MAT 会在结果里显示每个实例的浅大小和保留大小。WHERE子句支持访问字段,h.size就是 HashMap 的 size 字段。参数上,类名必须写全限定名,内部类用$分隔。OQL 执行较慢,大 dump 上跑要耐心,建议先用 Histogram 缩小类范围再写 OQL。
注意:OQL 里访问字段用的是字段名而非 getter,如果字段被混淆过(比如经过 ProGuard),名字会变成 a、b、c,这时候得结合类结构反推。
3.4 用 Path to GC Roots 锁定持有链
找到可疑对象后,右键选Path to GC Roots -> exclude weak/soft references,MAT 会列出从 GC Roots 到这个对象的完整引用路径。这条链就是「谁在攥着它不放」的答案。
举个典型场景:一个ThreadLocal里存了大对象,线程池的线程长期存活,导致对象一直可达。Path to GC Roots 会显示Thread -> ThreadLocalMap -> Entry -> value -> 你的对象,一眼就能看出是 ThreadLocal 没清理。排除弱引用、软引用是常规操作,因为这两类引用本身不构成真正的泄漏,排除后剩下的强引用链才是问题所在。
如果链特别长,可以在结果窗口里逐级展开,重点看那些「业务不该长期持有」的节点,比如静态 Map、缓存、监听器列表。
4. 避坑与排查:MAT 分析 hprof 的五个血泪经验
4.1 打开就 OOM,报 Java heap space
现象:选中 hprof 后 MAT 卡住,控制台或弹窗报OutOfMemoryError: Java heap space。
原因:MAT 自身堆内存不够,默认 ini 往往只给 1GB 左右,读大 dump 必然崩。
解决:改MemoryAnalyzer.ini的-Xmx,给到 hprof 大小的 1.5 到 2 倍。同时确认用的是 64 位 MAT。如果物理内存实在不够,先用jmap -histo缩小排查范围,或者用-XX:+HeapDumpOnOutOfMemoryError时配合-XX:HeapDumpGzipLevel压缩 dump 体积。
4.2 dump 文件比堆上限还大,怀疑文件损坏
现象:hprof 文件大小超过-Xmx设置,MAT 提示文件可能不完整或无法解析。
原因:dump 过程中 JVM 被 kill,或者磁盘写满,导致 hprof 尾部截断。hprof 是流式写入,中途中断就会损坏。
解决:用jhat或jmap无法修复损坏文件,只能重新 dump。预防手段是 dump 到独立磁盘分区,确保空间充足;OOM 自动 dump 时加-XX:+HeapDumpOnOutOfMemoryError的同时监控磁盘水位。
4.3 Leak Suspects 报告说「没发现泄漏」
现象:自动报告显示无 suspect,但内存确实在涨。
原因:泄漏对象被软引用/弱引用持有,或者泄漏是「缓慢累积」型,单次 dump 看不出,需要对比两次 dump。
解决:调整引用处理策略,把软/弱引用设为强可达再分析。更靠谱的做法是间隔一段时间 dump 两次,用 MAT 的「Compare Basket」功能对比两个 hprof,看哪些类的实例数在增长。对比功能在Histogram页右键Add to Compare Basket,然后打开对比视图。
4.4 支配树里找不到业务类,全是 char[] 和 byte[]
现象:Top Consumers 排前面的是char[]、byte[]、String,看不到自己的业务对象。
原因:这些是底层存储,真正持有它们的是上层的 String 或集合。直接看数组没意义,要看谁支配了这些数组。
解决:在 Dominator Tree 里,char[]的父节点往往就是持有它的 String 或 StringBuilder。展开父节点,顺着支配关系往上找,直到出现业务类。或者用 OQL 直接查业务类实例,绕过底层数组的干扰。
4.5 分析结果和监控指标对不上
现象:MAT 算出的总内存远小于监控里的堆使用量。
原因:dump 时触发了 Full GC,不可达对象已被回收,hprof 只包含存活对象;而监控指标可能包含未 GC 的垃圾。另外 MAT 默认不统计不可达对象。
解决:理解 hprof 是「GC 后快照」这个前提,它反映的是真实存活集。如果要对齐监控,看 dump 时刻的 GC 日志,确认 dump 前是否发生过 Full GC。排查泄漏时,关注的是存活对象的增长趋势,而不是绝对值和监控完全一致。
5. 进阶技巧:用 MAT 的对比与 OQL 组合拳定位缓慢泄漏
单次 dump 分析适合「已经爆了」的急性泄漏,但线上更多是「每天涨一点、一周后 OOM」的慢性泄漏。这种案子单看一个 hprof 往往看不出问题,因为每个对象看起来都「合理」。我的做法是间隔 30 到 60 分钟 dump 两次,用 MAT 做差分对比。
具体操作:打开第一个 hprof,在 Histogram 页选中所有类,右键Add to Compare Basket;再打开第二个 hprof,同样加入 Compare Basket;然后点 Compare Basket 窗口的Compare按钮,MAT 会生成一张对比表,列出每个类的实例数变化和内存变化。按实例数增量降序排,排最前面的类就是泄漏嫌疑最大的。
对比表里有个细节要注意:# Objects列显示的是两个 dump 的实例数差值。如果某个类差值很大且持续增长,基本可以锁定。但别只看绝对值,要结合业务判断——比如String增长可能是正常的日志缓冲,而某个自定义 DTO 增长就值得警惕。
配合 OQL 可以进一步确认。假设对比发现com.example.dto.OrderContext实例数从 1000 涨到 50000,可以写 OQL 查这些实例的某个字段分布:
-- 统计 OrderContext 实例按 status 字段分组 SELECT o.status, COUNT(*) FROM com.example.dto.OrderContext o GROUP BY o.status逻辑说明:OQL 支持GROUP BY和聚合函数,能快速看出泄漏对象集中在哪个状态。如果发现全是status = 'PENDING'的实例,那问题就指向「订单完成后没有清理上下文」这类业务逻辑缺陷。参数上,COUNT(*)返回分组计数,字段名要和类定义一致。
还有一个我常用的技巧:在 Dominator Tree 里对某个可疑对象右键Show Retained Set,MAT 会高亮显示这个对象支配的所有对象集合。如果这个集合里全是同类型的业务对象,说明这个「容器」就是泄漏的根。比如一个静态ConcurrentHashMap的 Retained Set 里全是Session对象,那就是会话没清理。
最后说个验证方法:定位到嫌疑代码后,别急着改,先在本地用-XX:+HeapDumpOnOutOfMemoryError复现,dump 出来用同样的 MAT 流程走一遍,确认支配树里的路径和线上一致。线上和本地环境不同,类加载器、线程池配置都可能影响引用链,本地复现能排除环境干扰。
从那以后我每次拿到 hprof,都强制先改 MAT 的-Xmx再打开,然后按「Top Consumers 找大类 → Dominator Tree 找实例 → Path to GC Roots 找持有链」三步走,最后用两次 dump 对比确认趋势。这套流程帮我省下了无数个对着乱码文件发呆的夜晚。希望帮到你。
本文还有配套的精品资源,点击获取