简介:这是一套专门分析Java堆转储文件的MAT工具包,面向需要排查内存泄漏、优化堆内存使用的Java开发与运维人员。工具可直接解析JVM导出的hprof文件,借助支配树、保留堆统计、泄漏嫌疑人报告和对象查询等视图,快速定位内存占用异常的类与对象引用链,适用于线上故障排查、性能调优和代码审查。压缩包共约七十五兆,内含两千个文件,其中以运行库、网页帮助文档、图示和配置信息为主,既包含可执行的插件程序,也携带完整的操作说明与图解,还附有一份示例堆转储文件,方便离线安装、查阅和反复练习。借助这些材料可以完成从堆快照导出、对象占用排序到泄漏根因确认的完整分析流程。目前已有六千六百一十三人学习下载,适合希望系统掌握堆内存分析路径并快速排除内存瓶颈的初级与中级Java工程师。
1. MAT 与 hprof:一份堆转储到底能帮你查到什么
如果你维护过 Java 后台服务,大概率经历过这种场景:某天线上进程内存一路飙升,Full GC 频次肉眼可见地变密,直到某个凌晨 OOM 崩溃,重启后又恢复正常,但没人说得清是谁占用了内存。这种情况下最有效的排查手段,就是在进程还活着或即将崩溃时留一份 hprof 堆转储文件,然后用 MAT 打开它。MAT(Memory Analyzer Tool)是目前 Java 生态里最常见的堆转储分析工具,hprof 则是 JVM 直接落盘的堆快照格式;两者的组合,能把黑匣子一样的堆内存打开,让你看清楚到底是谁在“吃掉”内存。
我自己的血泪经验是:与其靠 Log 猜、靠代码 review 找,不如先拿到一份 hprof 再说话。MAT 最核心的价值不是告诉你“内存很大”,而是通过支配树和保留大小,把“谁持有谁”的引用链呈现出来,让你能从对象层级里精准定位到泄漏源头。适合读这篇文章的人很具体:正在处理内存泄漏、频繁 Full GC、或刚拿到一份 hprof 却不知道怎么下手的人。你不需要会调 JVM 才能读完,但读完以后,你会知道这份文件从哪里来、用什么姿势打开、哪些按钮真正有用,以及为什么会翻车。
2. 拿到 hprof:线上生成堆转储的三种常用方式
MAT 分析的是 hprof,但很多人第一步就卡住了:手里根本没有一份 hprof 文件。所以先讲清楚怎么把 Java 进程的内存快照落下来。常见做法是使用 JDK 自带的命令和 JVM 参数,不需要额外装任何 agent。
2.1 jmap 生成本地转储:命令行三步走
jmap 是 JDK 自带的内存映射工具,它可以触发一次堆转储并生成 .hprof 文件。最常用的命令是:
jmap -dump:live,format=b,file=/opt/dump/heap_20250101.hprof <pid>其中<pid>是 Java 进程的进程号,可以用jps -l查看。live表示只导出存活对象,format=b表示二进制格式,file指定输出路径。生成后,你会得到一个以 hprof 为格式的二进制文件。
注意几点:第一,live会先触发一次 Full GC 再快照,意味着你看到的堆内容是“清洗后”的,更适合排查存活对象的持有关系;如果你怀疑是短命对象、或者需要分析 GC 后的残留,二者都可取。不过想分析原始分配情况,建议去掉live,即:
jmap -dump:format=b,file=/opt/dump/heap_raw.hprof <pid>区别在于是否强制 GC。线上执行 jmap 有短暂停顿,特别是大堆,所以尽量在低峰期操作,并且别对正在承载高并发的核心节点反复执行。
2.2 JVM 参数自动转储:让 OOM 前自动落盘
我更推荐在启动参数里提前埋好转储开关,因为大部分线上事故是毫无征兆的。配置两个参数,OOM 那一刻 JVM 会自动生成一份 hprof,不用你赶去敲命令。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/oom_$(date).hprofHeapDumpOnOutOfMemoryError决定 OOM 时是否自动转储;HeapDumpPath指定落盘路径。注意$(date)这种写法依赖启动脚本的 shell 能力,如果是 systemd 管理进程,更稳妥的做法是把日期写死在脚本变量里,避免目录解析失败。
这个参数组合是很多团队的标配。好处是转储时机和 OOM 完全同步,你看到的堆状态就是崩溃前的现场。缺点是如果堆很大,写盘过程可能持续十几秒甚至更久,进程处于假死状态。运维上建议把转储目录放到独立磁盘,避免写满系统盘影响其它服务。
2.3 jcmd:不想中断进程时的备选
jcmd 是比 jmap 更现代的诊断命令,可以在不退出进程的情况下执行很多操作,其中也包含堆转储。我一般遇到需要反复测试转储操作时会优先用 jcmd,因为它的反馈更友好,也不容易被权限问题挡住。
jcmd <pid> GC.heap_dump /tmp/heap_20250101.hprofjcmd 会把结果直接打印成一段文字,告诉你“Heap dump file created”。与 jmap 相比,它更贴近 JDK 官方推荐。两者生成的都是标准 hprof 格式,MAT 都能打开。实际选择上,哪个顺手用哪个,关键是别在进程已经卡死时才想起执行。
2.4 hprof 文件里到底存了什么
hprof 不是普通文本,而是一种二进制快照,里面包含了每个 Java 类的实例数、每块堆空间的对象分布、对象之间的引用关系,还有部分线程栈和运行时常量池的信息。正因为有了引用关系,MAT 才能绘制出支配树、计算保留大小。说白了,hprof 是一张堆内对象的“全家福”,不止有尺码,还标了谁挂在谁下面。读 hprof 时,你会发现它体积可能是堆大小的数倍,比如 4G 堆可能生成 6G 甚至更大的文件,因为对象头、引用关系都要写进去,这在后续启动 MAT 时是关键考量。
3. 首次用 MAT 打开 hprof:安装、界面与第一份分析报告
拿到 hprof 文件后,真正的重头戏开始了。MAT 的下载和使用并不复杂,但第一步必须想清楚一个事:MAT 自身不是 JVM,它是个独立的桌面程序,却要读取比你本地可用内存还大的二进制文件。所以安装完第一件事不是双击打开,而是改它的内存配置。
3.1 安装与 MAT 自身内存设置
MAT 可以从主流开源镜像站下载独立压缩包,解压即用,不需要安装。它依赖本机 JDK,版本建议与生成 hprof 的 JDK 相近或更高,避免堆数据结构版本差异导致解析失败。解压后,关键文件是MemoryAnalyzer.ini,在安装根目录下。
-Xmx4g -Xms1g这两行都在MemoryAnalyzer.ini里,-Xmx4g是 MAT 分配给自己的最大堆内存,-Xms1g是初始堆大小。如果你的 hprof 是 4G,MAT 自身堆至少给到 4G;hprof 是 8G,就给 8G。经验上-Xmx至少等于 hprof 文件大小,因为解析过程中 MAT 会在内部建立索引,需要预留足够的空间。我见过很多人在这一步没调,结果打开大 hprof 秒变 OOM,页面疯狂转圈,最后只能强制杀进程。这算最典型的开局翻车。
3.2 首次打开 hprof:界面到底想告诉我们什么
双击或通过 File -> Open Heap Dump 打开.hprof,如果文件很大,MAT 会先进入 “Parsing heap dump” 阶段,耐心等它跑完,越大的 dump 时间越长,不是卡死。解析完成后进入 Overview 页面,这一页已经很能说明问题了。页面顶部有图表显示堆占用总大小、类数量、对象数量,紧接着是两个核心入口:Actions 区和 Reports 区。
第一步建议先看Overview右侧的Size图标,它会按类列出实例数和 Shallow Heap(浅堆大小)、Retained Heap(保留大小)。对新手来说,比较容易理解的是表格排序:点Retained Heap列按降序排列,你会立刻看到占据内存最多的类。如果排第一的是byte[],往往意味着有大量字符串或文件缓冲堆积;如果java.lang.String本身巨大,则考虑 String 常量池或缓存异常。但这只是起手式,真正有价值的是下一步的自动化报告。
3.3 Leak Suspects:MAT 的自动诊断报告
在 Overview 页点Leak Suspects,MAT 会花几十秒到几分钟做一次自动嫌疑分析,然后生成报告。这份报告会列出若干嫌疑人,每个嫌疑人下面有一段解释,说明为什么怀疑它,以及被什么对象整体引用。核心看两个指标:Retained Heap和Percentage,分别表示这个嫌疑对象整体保留了多少内存、占堆百分比多大。
Problem Suspect 1 The class "java.util.HashMap$Node[]", loaded by "system class loader", occupies 1,234,567,890 (40%) bytes. The memory is accumulated in one instance of "java.util.HashMap$Node[]" loaded by "system class loader".这种输出方式把“症状”包装成一句话,后面还有StackTrace和References解释嫌疑对象被谁引用。顺着引用链看,你会发现大部分内存泄漏最终都是某个静态容器没释放,或者缓存类对象不断膨胀。Leak Suspects 的价值是帮你快速划定排查范围,但它有时候会误报,把大缓存当作泄漏,所以还得用下一章的手动链路去验证结论。
4. 从直方图到支配树:手动定位内存泄漏的操作流
自动报告只是引路,真正要确认泄漏链,必须动手查。这一章介绍我在实际排查中固定使用的三步操作:直方图找巨头、支配树追持有链、OQL 精准定位。
4.1 直方图:先定位到具体类
Overview 里点击Histogram,会得到一张所有类的实例数量与内存占用表。默认按浅堆排序,你需要先把它切换成按Retained Heap排序。保留大小代表如果把这个类的所有对象全部回收,堆能释放多少内存,它是泄漏判断的核心指标。
实际操作中,我按这个顺序查:
Retained Heap > 100MB 的类如果一个类的保留大小超过堆总大小的 20%,直接进下一个环节查实例。需要说明的是,Retained Heap包含了被它独占引用的下层对象,比如一个 HashMap 保留了几百万个 Entry 和它们的 key/value,那么 HashMap 的 Retained Heap 就会很大。这个数字大,恰恰说明它持有的数据量大,而不是它本身占了多少字节。
4.2 支配树:找出谁在最顶端持有这一切
直方图无法回答“这个类为什么没被回收”,所以接下来右键这个类,选择Path To GC Roots,然后选exclude weak references。这一步会展示从 GC Root 到这个类实例的完整引用链。GC Root 通常是静态变量、线程栈变量或 JNI 引用。如果引用链上出现了某个业务对象或静态集合,那基本就找到持有者了。
有时候引用链很长,阅读起来费劲,这时换到Dominator Tree视图。支配树会把对象之间的关系简化成父子结构,任何一条子链路的顶级父节点,就是这个“家族”的总持有者。我经常这样操作:
- Histogram 里找到内存占用最大的类,右键 -> Show Objects By Class -> By Outgoing References;
- 在
Dominator Tree列里看它直接和间接持有的子对象; - 确认这个类的生命周期是否应当常驻。
比如在某后台系统里,看到java.lang.ThreadLocal相关对象占据大量内存,顺着支配树发现是一个线程池里的 ThreadLocal 没有 remove。这种问题在代码世界里看一万遍也看不出,但支配树一画,清晰可见。
4.3 OQL:用查询语句进行精准狙击
MAT 内置了类似 SQL 的 OQL 查询能力,可以在大量实例里精确过滤你要找的对象。界面入口是工具栏上的OQL按钮,点开后输入查询语句,点击执行即可。下面是一条实际有用的查询:
SELECT * FROM java.lang.String s WHERE s.value.length > 100000这条语句查出所有长度超过 10 万字符的字符串。在分析大报文、大日志场景时,你会惊讶地发现系统里堆积着海量超大字符串,而这些往往就是 hprof 里最大的单块占用。
再如,怀疑某个业务对象数量异常膨胀时,可以用:
SELECT CONCAT('对象数量=', COUNT(*)) FROM com.example.OrderCachecom.example.OrderCache替换成你的全限定类名。OQL 还能配合OBJECTS关键字继续追溯引用:
SELECT OBJECTS SRC.owner FROM INSTANCES com.example.OrderCache重点提醒:OQL 是筛选工具,不是分析工具,它不能替你得出结论。我在实际使用中,通常先用 OQL 缩小到一批对象,再回到支配树确认它们被谁引用。两者结合,才算是完成了从“哪个类大”到“为什么大”的闭环。
5. MAT 分析 hprof 的五个典型坑
下面几条坑都是我从实际排查里攒下来的,遇到了别慌,基本都有固定解法。
5.1 现象:打开 hprof 后 MAT 直接发疯,CPU 飙满、秒退
原因几乎都是MemoryAnalyzer.ini里-Xmx太小。本地默认可能只有 1G,遇到 3G 以上的 hprof 直接崩。解决方式很粗暴:先把 ini 里的-Xmx调到 hprof 文件体积的 1.5 倍左右,并保证本机物理内存够用。如果 hprof 特别大,比如几十 G,建议改用服务器模式,或用 MAT 的批处理命令在 Linux 环境跑分析,内存配置同理会生效。
5.2 现象:Leak Suspects 报的嫌疑人和线上现象对不上
你以为泄漏的是业务缓存,报告给出的嫌疑人却是某个基础类库的对象。原因在于 Leak Suspects 是基于支配树和引用链做的启发式分析,它会把保留内存最大的根子对象当嫌疑,不认识业务语义,所以误报不可避免。解决方式:把它当线索而非结论,动手去确认引用链。有一个百试百灵的判断方法:查看嫌疑人的Path To GC Roots,如果引用链是以java.lang.Thread结尾,意味着数据挂在线程上,通常需要检查线程池和 ThreadLocal;若是以静态容器结尾,再去查谁在往容器里写数据。
5.3 现象:hprof 文件巨大,MAT 解析要几十分钟
原因有两个,一是 open 前没给 MAT 足够内存,导致频繁 GC 减慢解析;二是 hprof 本身包含大量不可达对象。解决方式,先用jmap -dump:live重新生成,排除不可达对象;或者把 hprof 拆成两个时段对比,而不是一股脑全部分析。还可以使用 MAT 在启动时加参数-vmargs -Xmx8g再一次确认黑匣子容量,往往能让解析时间缩短一大半。
5.4 现象:对象数量正常,但 Retained Heap 奇大
这通常不是泄漏,而是 byte[] 或 char[] 被大块分配后没释放。举个例子,IO 流读文件时如果用字节数组一次性读进内存,多个线程同时执行时,hprof 里会看到一堆byte[]。这种情况代码上需要改分批读取,而不是调整堆大小。另一个常见隐藏对象是java.nio.HeapByteBuffer,它内部持有 byte[],会体现在支配树上,别光看类型名。
5.5 现象:本地打开的 hprof 和线上表现不一致
线上明明已经 OOM,本地打开 hprof 一看,堆里残留对象并不多。原因可能是 dump 时线上进程已经处于 OOM 边缘,大量内存被 GC 回收到不可达状态,而 live 模式只导出存活对象,反而不容易看到“犯罪现场”。解决:下次保留一份不带 live 的全量 dump,同时记录当时的堆使用率和 Full GC 频率。我个人的习惯是线上用jmap -dump:format=b,file=xxx保留原始现场,本地再结合 GC 日志互相印证。
6. 把 MAT 用得更顺手:两个步骤与一个习惯
分析 hprof 到这个阶段,你已经具备了完整排查链路。但想长期不被 OOM 折磨,还得把“分析”变成“例行动作”。我有两个步骤强烈建议固化到团队流程里。第一,所有服务启动参数里统一预埋-XX:+HeapDumpOnOutOfMemoryError,并把转储路径写到独立磁盘,同时设定一个 cron 巡检,每天检查转储目录大小,防止文件写满磁盘。第二,在每次发布涉及缓存、连接池的代码前,先在压测环境模拟一次满载请求生成 hprof,然后跑一遍 Leak Suspects。这两个动作一次投入,后续每次排查至少能省下半天时间。
最后一个习惯,是我自己被坑了多次后总结出来的:每次都在 MAT 的报告里做一次“三行记录”,一行写堆总大小、一行写 Retained Heap 排名前五的类、一行写引用链结论。这些数据可比日志里的错误堆栈真实得多,也是下次版本对比最好的素材。时间长了,你会对本系统的内存习性非常清楚,看到某个类一出现在第一位就知道刚发布的那批代码踩雷了。内存排查从来不是什么玄学,只是要有一份好的 hprof,和一个会用它的工具。希望这段内容能帮你少走几个弯路,在下次排查时稳稳落地。
本文还有配套的精品资源,点击获取