简介:Eclipse Memory Analyzer Tool(MAT)是一款Java堆内存分析工具,这份压缩包提供其在Windows环境下的完整发行版,面向排查内存泄漏、优化JVM性能的Java开发者。包内共5442个文件,压缩包大小约133.56MB,含3758份HTML帮助文档、355个JAR插件、422张PNG界面图片及XML/Properties配置文件、批处理脚本和示例hprof转储文件,完整覆盖MAT运行所需组件。已有368人学习/下载。借助该工具,可加载JVM堆转储,生成Leak Suspects、Dominator Tree、Histogram等报告,清晰展示对象引用链和内存占用,辅助定位内存泄漏源头;同时目录结构完整,便于离线部署与二次配置,适合日常开发及生产环境问题排查。 排查线上 OOM,很多人习惯先去翻日志,但日志末尾往往只剩一句java.lang.OutOfMemoryError: Java heap space,真正的现场早被 GC 清掉了。eclipse mat(Eclipse Memory Analyzer Tool)不读文本日志,它分析的是 JVM 在快撑不住时导出的 heap dump——相当于内存的 X 光片。
这个工具擅长把几 GB 的堆快照切成可视化的「谁占了多少、谁还被谁引用着」,让泄漏点从几千个对象里浮出来。被线上告警逼着加班的后端开发,或者刚接手内存排查的新人,都能用它省下大半个晚上。
下面按我自己的排查路径写:dump 怎么拿、MAT 怎么配、嫌疑报告怎么读、怎么人肉确认、有哪些坑。走完一遍,独立排查一次堆内存问题基本够用。
2. 从 dump 到 MAT:堆快照的四种获取方式与打开前必改的三个配置
2.1 线上 OOM 自动留证:两个 JVM 参数提前埋好
生产环境最怕的不是 OOM 本身,而是 OOM 发生后进程被拉起,现场全没了。所以我会在启动参数里提前埋好两个开关,让 JVM 在抛出 OutOfMemoryError 之前自动把堆快照写下来:
java -Xmx2g \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/dump \ -jar app.jar这段启动参数的逻辑很简单:-XX:+HeapDumpOnOutOfMemoryError是开关,打开后遇到 OOM 会自动导出堆;-XX:HeapDumpPath指定导出目录。生成的文件命名规则是java_pid<pid>.hprof,pid 是进程号,同一个目录下不会互相覆盖,看到文件名就知道是哪个进程留下的。
这里有两个细节值得单独说。第一,HeapDumpPath 指向的目录要选容量充足的独立磁盘,因为 dump 文件大小和-Xmx配置的堆上限相当,2G 堆就可能产出 2G 左右的 .hprof,放在系统盘很容易把磁盘打满。第二,如果是容器部署,记得把这个目录挂载出来,否则容器一销毁,dump 跟容器一起消失,参数等于白配。
除了 OOM 自动导出,我还会在告警脚本里补一步:内存指标连续超过阈值时,用 jcmd 主动导一次,保留「还没完全崩掉但已经很难受」的现场。这个习惯帮我抓过好几次缓慢泄漏,后面第 6 章会展开讲。
2.2 手动导出:jmap 和 jcmd 要分场景用
线上没挂自动导出参数,或者已经收到告警但还没崩溃时,需要手动导出。常见做法是 jmap 或 jcmd 二选一,我一般这样用:
# 方式一:导出前先做一次 Full GC,只保留存活对象 jmap -dump:live,format=b,file=/data/dump/live.hprof <pid> # 方式二:不触发 GC,直接导出当前堆的全部内容 jcmd <pid> GC.heap_dump /data/dump/manual.hprof两条命令的差别,很多人第一次用注意不到:jmap -dump:live会先触发一次 Full GC,把堆里可回收对象清掉再导出,所以你拿到的堆是「清理后的稳态」,那些还没来得及回收、正在膨胀的对象看不到了。jcmd <pid> GC.heap_dump不带 live 语义,导出的是当前堆的真实内容,更接近崩溃前的现场。
所以在排障场景里,我默认用 jcmd。只有明确想对比「存活对象占用」时才用 live 方式,比如排查线程池堆满、静态集合膨胀这类问题。另外注意一点:导出大堆时应用会暂停几秒到几十秒,属于正常现象,最好选业务低峰期操作,并在执行前先df -h确认磁盘余量。
2.3 打开 dump 前:MAT 自身的堆先给足
MAT 本身是用 Java 写的桌面程序,分析几 GB 的 .hprof 时,它自己的内存要够大,否则第一关就是「MAT 打开 dump 自己先 OutOfMemoryError」。
文件位置在安装目录下的 MemoryAnalyzer.ini,把这段改掉:
-Xmx4096m我给内存的建议是一个经验值:-Xmx配到 dump 文件大小的 1.5 到 2 倍,但不要超过本机物理内存的一半。比如本机 16G,dump 4G,就配 6144m 或 8192m;如果电脑只有 8G,dump 却有 6G,那别硬开,建议换一台内存大点的机器,或者用后面会讲的命令行模式跑分析。同时确认你启动 MAT 用的是 64 位 JDK,32 位 JDK 堆上限很低,大 dump 基本无解。
文件改完,打开操作就没有什么技术含量了:File → Open Heap Dump,选中 .hprof 文件。第一次打开大文件会构建索引,进度显示在右下角,等几分钟很正常;索引构建完成后同目录会生成 .index 等辅助文件,第二次再开同一个 dump 会快很多。
2.4 现场没了怎么办:兜底方案
最尴尬的情况是:告警来了,进程自动重启,dump 没配或者没导出来。这时候没有直达的 X 光片,只能靠两个兜底。
一个是在内存告警阶段就主动触发导出,哪怕此时堆还没溢:只要能连上进程,jcmd 就能导出,别等到 OOM 才动手。另一个是把 JVM 参数加上-XX:+ExitOnOutOfMemoryError或-XX:+CrashOnOutOfMemoryError,让 OOM 发生时进程直接退出而不是继续硬撑,同时配合自动导出。这样至少保证「崩之前留证据」的链路是完整的。
数据都拿不到时,MAT 再强也分析不成,这也是为什么我会把 HeapDumpOnOutOfMemoryError 和告警脚本一起纳入每个服务的发布模板。先有现场,才有分析。
3. 进 MAT 先读嫌疑报告:Overview 与 Leak Suspects 的实战读法
3.1 Overview:从饼图开始,先抓住「最大的那一坨」
打开 dump 后默认落在 Overview 页,这一页就够读出七八成信息。上半部分是 pie chart,按 retained size 展示堆里对象的大小占比;下半部分 Details 区域列出 total heap、classes、objects 三个总数;右侧 Actions 是跳转到 Leak Suspects、Histogram、Dominator Tree 的入口。
读法上我从不纠结总数,直接看饼图最大的扇区是哪个类型。扇区越大,说明这类对象是「拽着内存不放」的主力。对应关系大致如下:
| 饼图最大的类 | 常见指向 | 下一步动作 |
|---|---|---|
| char[] / byte[] | 文本、序列化缓冲、报文存储 | 用 Histogram 查持有者 |
| 业务对象数组(Task[] 等) | 批量任务堆积、队列积压 | 用 Dominator Tree 找源头 |
| java.lang.Thread | 线程数量失控,栈内存超限 | 直接看 Thread Overview |
这里的 pie chart 是基于 retained size 画的,不是 shallow size。原因很简单:shallow size 只算对象自己,ArrayList、HashMap 这类容器对象自身很小,浅堆排序会把它们排到后面,但它们背后拽着的元素可能占掉大半个堆。用 retained 视角,容器类才会浮到前面。
3.2 Leak Suspects:机器圈了嫌疑,别急着抄结论
Overview 的 Actions 区域点 Leak Suspects,MAT 会自动跑一遍分析,给出嫌疑列表。每条嫌疑包含一段描述、Shortest Paths To Accumulation Point(到累积点的最短路径)和 Accumulator(累积对象)。
我实际遇到的一个典型描述长这样:某订单批量任务的上下文对象持有了 12,000 个订单快照对象,累计约 1.1GB。第一反应是订单快照泄漏,但别急着下结论。这个描述只能说明「有强引用路径把这些对象撑在堆里」,至于这 12,000 个对象是必然存在的业务数据,还是失控堆积的垃圾,报告不会替你判断。
正确读法是展开 Shortest Paths,看引用链最顶端是什么:如果顶端是静态集合、ThreadLocal 字段、或者某个长生命周期单例的成员变量,基本朝泄漏方向查;如果顶端是业务线程栈、请求上下文、任务队列,更有可能是并发量上去后临时撑大的,需要结合业务再定性。Leak Suspects 的价值在于缩小范围,不在于直接给结论。
3.3 Thread Overview:还原 OOM 前一刻的线程现场
排查时我习惯在看嫌疑报告前先花一半时间看 Thread Overview。这个视图把 dump 时刻所有线程的栈和持有对象列出来,能回答一个关键问题:OOM 前那一刻,线程们到底在做什么。
典型场景是这样:某个拦截器把请求上下文塞进 ThreadLocal,用完没 remove;线程池里的线程是复用的,一次两次无所谓,几千次请求后每个线程的 ThreadLocal 里都堆着一串过期上下文。这种问题看 Histogram 要在几百个类里猜,而 Thread Overview 里直接能看到线程栈停在拦截器的哪一行,ThreadLocal 字段里挂着多少对象。
拿到这个线索,再回去看嫌疑报告和直方图,基本就是实锤了。要注意的是,Thread Overview 只能反映 dump 时刻的状态。如果 OOM 发生在业务凌晨的低峰,dump 时刻堆已经「冷静」下来,线程栈看到的可能是一片空闲,这时候该信嫌疑报告而不是线程视图。
4. 人肉确认泄漏点:Histogram、Dominator Tree 与 GC Roots 的组合拳
4.1 Histogram:先把 Retained Heap 列排到最前
Overview 右侧 Actions 点 Histogram,进入类维度的统计表。这张表有四个关键列:Class Name、Objects、Shallow Heap、Retained Heap。
提示:Shallow Heap 是对象自身大小,Retained Heap 是「这个对象被回收时能释放多少」。业务排查只看 Retained Heap,先从这一列排序。
实际操作中,我拿到 Histogram 的第一件事是点 Retained Heap 列头降序,然后盯着前三行看。给你一个典型的视角:
| 类名 | Objects | Shallow Heap | Retained Heap |
|---|---|---|---|
| java.lang.String | 450,000 | 18 MB | 42 MB |
| 某订单快照 | 12,000 | 1.2 MB | 1.1 GB |
| java.util.HashMap | 300 | 0.5 MB | 800 MB |
这张表的信息量很大:String 数量 45 万但 retained 才 42MB,属于正常范畴;某订单快照 12,000 个对象占用 1.1GB,这是第一嫌疑人;HashMap 只有 300 个实例却留着 800MB,说明是某个大 Map 挂着一大串数据,典型的不设容量上限的缓存或批量聚合。下一步不是猜,而是进 Dominator Tree 去拆引用链。
Histogram 还支持按类名片段过滤。输入业务包的类名前缀,比如com.example.*Snapshot,能瞬间滤掉 JDK 内部类,把视线集中在业务代码上。排查线上问题时,这一步基本是必做的。
4.2 Dominator Tree:顺着支配关系找到「拽着整片内存的人」
Dominator Tree 解决的问题是:某个对象很大,它是怎么大的?被谁以什么方式拽着?
进入方式有两种。一种是在 Histogram 里对目标类右键 → List objects → with outgoing references,先看这个类的实例往外引了什么;另一种是直接打开 Dominator Tree 视图,按 retained size 排序,看树的层级。
以 4.1 的某订单快照为例,我的操作路径是:
- 在 Histogram 里定位到该类,右键 → List objects → with outgoing references;
- 展开引用列表,看大头的引用是什么类型。如果全是 char[],说明问题在字符串数据上;如果引用了业务集合、子对象,继续往下钻;
- 切到 Dominator Tree,按 retained 排序,找出树根附近的对象——那才是真正把整片内存拽住的持有者。
Dominator Tree 的概念不用背术语:如果某个对象从 GC Roots 一断,它下面一大片对象全都跟着不可达,那这个对象就是它们的支配者。越靠近树根,越接近「源头持有者」。实际操作中,我经常在 Dominator Tree 里看到一个业务聚合对象站在树根,下面挂着几百个 Map 和数组,这种视图比 Histogram 直观得多。
4.3 Paths to GC Roots:用一条引用链给结论收尾
Histogram 和 Dominator Tree 能定位到对象,但「这个对象为什么没有被回收」还得看 GC Roots 路径。右键目标对象 → Paths to GC Roots → with all references,MAT 会列出所有能让它活着的引用链。
操作上有一个习惯很关键:先选 exclude weak / soft references。弱引用和软引用本来就允许被回收,它们导致的持有不是泄漏;把它们排除后,剩下的都是强引用链,条条都能落到实处。
典型结果是这么一种引用链:java.lang.Thread → threadLocals 字段 → 某拦截器上下文 → 业务对象。看到这条链,基本可以实锤是 ThreadLocal 没清理,而不是什么玄学问题。我已经不止一次在这种路径里发现「某个全局静态 Map 只增不减」的根因。
到这一步,排查闭环完成:嫌疑报告缩小范围、Histogram 锁定类、Dominator Tree 找持有者、GC Roots 路径确认根因。剩下就是改代码、发版本、再导一次 dump 验证。
5. eclipse mat 常见坑与排查:四处让我翻过车的细节
这几条坑不分先后,每条我都实际翻过一次车。写在这里,是希望你在排查现场少走弯路。
5.1 MAT 打开大 dump 自己先 OutOfMemoryError
现象:双击 .hprof 后,界面卡在解析阶段,日志窗口刷出 OutOfMemoryError,或者直接无响应。
原因:MAT 自身也是 Java 程序,默认-Xmx配得很小,碰上几个 G 的 dump,构建索引时内存就爆了。
解决:改 MemoryAnalyzer.ini 里的-Xmx,按 dump 大小的 1.5 到 2 倍给,同时确保用 64 位 JDK 启动。如果机器内存实在不够,用 MAT 的命令行模式导出报告:-consoleui -application org.eclipse.equinox.app这类带参启动可以在无图形界面下生成分析结果,内存占用更可控。命令行模式的具体参数比较绕,需要的时候再查一次即可,重点是把-Xmx这个习惯改过来。
5.2 Histogram 排名第一的 char[] 不是元凶
现象:Histogram 按 Retained Heap 排序后,char[] 占了两个多 G,排名第一,但代码里根本没有大段的字符串拼接或报文缓存。
原因:char[] 是 String 的底层存储,业务对象只要持有大量 String,就会在 Histogram 里汇成巨大的 char[] 扇区。它只是结果,不是原因。
解决:在 char[] 行右键 → Merge Shortest Paths to GC Roots,或者切到 Dominator Tree 看上两层,把大量 char[] 收敛到少数几个业务类上,再按 4.2 的方式去看持有链。我一开始犯的错误是盯着 char[] 想是不是 String 泄漏,绕了大半天才发现是某个缓存 Map 没上限。
5.3 对象 ID 后缀看不懂的时候,用 Inspect 面板看字段
现象:Dominator Tree 里同一个类出现几十个实例,每个都是com.example.Order@41a20e这种带十六进制后缀的名字,不知道哪一个才是要查的。
原因:@后面是 JVM 给对象的内部标识,和业务主键没有任何关系。几十个实例长得一样,光靠名字区分不出来。
解决:右键对象 → Inspect,在属性面板里看它的字段值,用业务主键(订单号、任务 ID)定位具体实例;或者右键 → Copy → Copy Value 拿到对象 ID,去 OQL 里按 id 反查。这个习惯能省掉大量「看着像但不敢确认」的纠结时间。
5.4 jmap -dump:live 导出的堆,不是崩溃时的堆
现象:同一应用,用 jmap 导 dump 分析出来的重点对象,和用 jcmd 导 dump 分析的结果完全不一样。
原因:jmap 的 live 选项会先触发 Full GC,堆里可回收对象被清掉后才导出,你看到的是「清洗后」的稳态,而不是「正在膨胀中」的现场。膨胀对象刚好处于可回收状态时,直接就被 GC 干掉了。
解决:排障场景默认用jcmd <pid> GC.heap_dump导出;只有明确关心「存活对象稳态分布」时才用jmap -dump:live。如果之前已经用了 live 方式,分析结论要打个折扣,重点看存活的大对象,不要拿它判断「哪些对象正在泄漏」。
5.5 排查节奏:先等索引,再定视图,后下结论
一条实打实的建议:大 dump 第一次打开会构建索引,这段时间别反复点视图,会让 MAT 更卡。正确节奏是打开后先等索引完成;然后按 Overview → Leak Suspects → Histogram/Dominator Tree → GC Roots 的顺序走,每一步都记录一个「嫌疑类名」,逐步收敛而不是四处乱点。乱点视图的结果往往是每个视图都看了,哪条线都没跟到底,最后只能重开 dump。
另外,分析完记得在 Overview 页把 JVM system properties 扫一眼,确认 dump 来自哪台机器、哪个启动参数,环境不对的话结论全部作废。这些坑大多不是 MAT 的缺陷,而是使用姿势的问题。工具本身很稳定,翻车的往往是在错误的现场导了错误的 dump,或者在错误的视图里下了错误的结论。
6. 两次 dump 对比与一条 OQL:把单次分析变成趋势排查
6.1 为什么需要两份 dump
单张 dump 只能告诉你「这个时刻堆长什么样」,回答不了「内存是不是在涨、涨的是哪一类」。缓慢泄漏的典型表现是:每次看 dump 都不至于 OOM,但堆水位一路爬坡。这时候最有价值的做法,是在业务低谷和高峰各导一份 dump,用 MAT 对比着看。
6.2 对比操作:Compare 与 Compare Base
MAT 的工具栏上有一对按钮:Compare Base 和 Compare。先打开第一份 dump,点 Compare Base;再打开第二份,点 Compare。MAT 会生成对比表,按类列出两份 dump 的对象数、Shallow、Retained 差异。直接按 retained 增量排序,增量最大的类就是上涨主力。
对比的两份 dump,取样时间要隔一个完整业务周期(比如一天或一周),并且尽量避开部署重启。如果中间发过版本,类加载器全换了,对比结果会失真。
6.3 一条 OQL,把嫌疑类的数字精确查出来
Histogram 的优点是全量,缺点是精度被样本稀释。要单独统计某个类的真实占用,用 OQL:
SELECT OBJECTS a AS instances, COUNT(a) AS count, SUM(a.@retainedHeapSize) AS retainedBytes FROM com.example.OrderSnapshot a这条查询返回三列:实例数、浅堆总和、保留堆总和。@retainedHeapSize是 MAT 对 OQL 的扩展属性,标准 SQL 没有这个语法,写的时候别拿别的工具的习惯套。把同一个查询在两次 dump 里各跑一遍,两张数字一对比,趋势比任何报告都直白。
我现在的习惯是:每次压测结束都留一张 dump,业务低谷也留一张做基准,线上 OOM 时有这两张底图,拿着 MAT 对比比从零查快得多。统计是死的,引用链是活的,MAT 不帮你修代码,但能把「疑似内存问题」证成「这条引用链就是根因」。希望帮到你。
本文还有配套的精品资源,点击获取