资讯动态

深入Eclipse MAT:用堆转储破解JVM OOM内存泄漏之谜

发布时间:2026/10/9 12:26:51 来源:尧图企业网站定制
简介面向 Java 开发、运维及性能调优人员的内存分析工具包基于 Eclipse MAT 构建专门解析 JVM 堆转储快照可精准定位内存泄漏、追踪对象引用链是排查线上内存溢出与性能瓶颈的常用利器。压缩包约 133.56 MB共 5442 个文件以 HTML 帮助文档、JAR 插件及 PNG/GIF 示意图为主体同时包含 EXE、DLL、BAT 启动与批处理文件以及 HPROF 格式的示例堆快照配置文件、p2 插件库、工作区等目录一应俱全既可独立启动运行也可作为 Eclipse 插件集成使用。已有 367 人学习下载工具内置 Leak Suspects 报告、支配树、直方图、重复对象检测等多种分析视图能够直观呈现内存占用、对象持有关系及引用链帮助读者结合示例堆文件快速掌握从堆快照加载到定位内存问题根源的完整流程。借助该工具包可减少自行搭建环境的时间直接上手分析真实堆转储数据为 Java 应用的内存优化与故障排查提供有力支撑。1. eclipse mat 日志分析工具它不查日志查的是日志背后的堆转储线上服务半夜报 OutOfMemoryError日志里只有一行异常堆栈服务随即重启。你翻遍了应用日志除了这行之外什么线索都没有接下来要查的是“到底谁把内存吃完了”。Eclipse MATMemory Analyzer Tool就是干这个的但必须先明确一个前提——它不读日志文件它分析的是 JVM 崩溃或 OOM 时导出的堆转储heap dump。这个二进制快照里存着那一刻所有存活对象的类型、数量和引用关系MAT 能把这些关系画成支配树、直方图和 GC Roots 路径让根因落到一个具体的类或一处集合。本篇按照一线排查流程来写堆转储怎么拿、MAT 怎么跑、内存参数怎么调、哪些坑会挡住你。适合后端开发、运维以及刚接触 JVM 内存分析的从业者。2. 堆转储从哪来用 jmap 与 JVM 参数把内存快照拿到手2.1 为什么必须先有堆转储MAT 才有分析对象Java 进程的内存划分成堆、栈、元空间等区域OOM 大多数情况下发生在堆上。日志里能记录的只有异常类和堆栈行号堆栈能告诉我们“哪个方法正在分配”却回答不了“谁长期持有这些对象”。打个比方一个房间里堆满箱子日志只告诉你箱子超重了而堆转储是给整个房间拍一张全景图连箱子和箱子之间的绳子都拍进去。MAT 就拿着这张图去剪绳子找到哪根绳是导致内存不释放的根因。因此分析的内存快照必须在问题发生时刻或尽量接近问题时刻获取否则关键对象可能已经被回收分析结果会失真。获取堆转储常见做法是两种一种在 OOM 发生前就埋好 JVM 参数让 JVM 自动生成文件另一种用 JDK 自带工具主动导出。下面给出一线用的最多的命令组合。2.2 生产环境生成堆转储jmap 命令与 JVM 启动参数先看进程再导出堆转储。JDK 自带两兄弟jps列出 Java 进程jmap导出内存快照。最小可用命令如下# 列出所有 Java 进程找到目标服务的 PID jps -l # 导出堆转储全量导出不触发 Full GC jmap -dump:formatb,file/data/heap_$(date %Y%m%d_%H%M%S).hprof PID # 导出堆转储并只保留存活对象会触发 Full GC jmap -dump:live,formatb,file/data/heap_live.hprof PID逻辑说明formatb表示二进制格式MAT 认这种格式也是绝大多数分析工具的标准格式file指定导出路径文件名里加上时间戳避免后面多个 dump 互相覆盖live参数会先触发一次 Full GC把不可达对象清掉再导出存活对象。如果只是为了找“哪些对象还活着”live会省分析时间但代价是触发一次全局停顿生产环境需谨慎。参数说明PID来自jps -l的输出不要用操作系统的进程号ps 的 PID因为它和 JVM 进程号通常一致但偶尔会混淆导出目录要有足够磁盘空间一般建议至少保留堆大小的两倍空间因为 MAT 分析时还要新建索引文件。导出的文件是二进制千万不要用文本编辑器打开会看到乱码且可能卡死编辑器。2.3 区分 Heap Dump 与 Thread Dump别混淆排查方向很多做日志分析的人拿到 OOM 第一反应是jstack抓线程栈看是否死锁。线程栈是线程执行状态快照回答的是“线程卡在哪一行”和“对象堆积在哪”是两码事。OOM 时线程栈里有大量分配请求但真正的原因是某个集合在后台持续增长线程栈只会指向分配点不会指向长期持有者。我的习惯是如果现象是 CPU 飙高或者线程卡死用jstack如果是java.lang.OutOfMemoryError: Java heap space用堆转储。有时两个文件可以配合着看但不要拿线程栈去代替堆转储。要养成在关键服务启动参数里提前准备好 OOM 自动导出的习惯这样日志里出现 OutOfMemoryError 时堆转储文件已经躺在指定目录里不需要再连到服务器上去手动抓。那么在生产环境怎么埋点在 JVM 启动参数中加入下面两个-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/heapdump.hprof逻辑说明HeapDumpOnOutOfMemoryError打开后只要 JVM 抛出 OOM 异常就会自动导出堆转储场景是“治病”而jmap是“体检”HeapDumpPath指定文件位置和文件名如果不写默认生成在启动进程的工作目录下找起来很麻烦。注意这个开关只对 OOM 有效对StackOverflowError无效。参数说明有些应用会限制目录写权限确保 JVM 进程对/data/logs有写权限否则 OOM 时文件创建失败又会在日志里增加一条新的错误。HeapDumpPath支持相对路径和绝对路径建议把路径固定下来后续做监控或定时清理都方便。3. 用 MAT 打开堆转储从 Leak Suspects 到 Dominator Tree 的排查路径3.1 打开堆转储后先看哪个视图Leak Suspects 自动报告拿到 .hprof 文件后启动 Eclipse MATFile - Open Heap Dump选择文件MAT 会弹出一个向导重点就是 “Leak Suspects”。这是 MAT 的自动分析模块它会把堆里占用最多内存的对象集合找出来按保留大小排序并用一张饼图展示嫌疑对象占比。对于第一次接触内存分析的同事我通常建议先看 Leak Suspects 报告因为它直接给出结论式的描述例如“一个实例 java.util.HashMap持有 68.2% 的内存”然后顺着右侧的 “Details” 展开就可以看到引用链。不过 Leak Suspects 报告并不是万能的。它基于支配树计算对于小而多的泄漏比如几万个监听器可能只会列出一个很大的聚合对象根因藏在某个集合元素的内部字段里。所以报告看完后必须进入下一个视图确认。3.2 Dominator Tree 与 Retained Size 的关系Open Heap Dump 向导里有一个Run Expert Report列表选择 “Dominator Tree”。支配树的核心概念是支配集如果一个对象 A 被回收另一个对象 B 也会跟着回收那么 A 支配 B。A 支配的所有对象加起来就是 A 的保留集MAT 用 “Retained Size” 表示保留集的大小。排列后最上面的对象往往是吞内存最凶的“头目”。使用这个视图时要注意排序字段默认按Retained Heap排序这个值代表若删除该对象能回收多少内存远比Shallow Heap有参考价值。Shallow Heap是对象自身占用的内存不包含内部引用比如一个空字符串可能是 40 字节但它可能被一个数组引用了上百万次浅堆太小但在直方图里会排前几。而 Retained Heap 会把引用链造成的空间算进去适合找根因。实际操作路径在 Dominator Tree 里选中一个对象右键菜单选择Path To GC Roots选with all references。这一操作会显示从 GC Root 到该对象的最短引用链GC Root 包括线程栈、静态变量、JNI 引用等。如果引用链很长说明对象不是泄漏源头只是被层层持有的结果如果链上出现ThreadLocal或Static标识往往就是“根”。这步是确认 Dominator Tree 结论必不可少的动作。3.3 直方图与 GC Roots 路径从数量到引用的双重验证直方图Histogram展示每个类的存活实例数量和总内存是一个带分类汇总的入口。很多新手一上来就在直方图里找最大对象却忽略“类大不代表是根因”的常见结论。比如java.lang.String可能占了几十 MB但那可能只是缓存的键真正持有缓存的是ConcurrentHashMap里的一个静态字段。因此直方图只用来做初筛选定一个类后右键Merge Shortest Paths to GC Roots是更快的验证手段它会跳过大量中间节点直接显示从 GC Root 到该对象的最短路径。GC Roots 路径视图有两类from all objects和exclude weak references。排查内存泄漏时优先看exclude weak references因为弱引用本来就允许被回收持有弱引用不构成泄漏。如果排除弱引用后找不到路径说明这个对象可能是可以被回收的临时对象不必深挖。这个方法比单纯在 Dominator Tree 里看列表更准确能帮你避免误杀诸如 Spring 容器之类的框架对象。下面用表格整理这三个视图的核心用途方便对照使用视图看什么典型用途Leak Suspects自动报告嫌疑对象和占比快速定位最可能的内存泄漏Dominator Tree每个对象的 Retained Heap找到支配大量内存的对象Histogram类的实例数和浅堆合计初筛数量异常的类Path to GC Roots强引用链上的持有者确认谁是真正不可回收的根实际操作顺序一般是Leak Suspects 给方向Histogram 看数量异常Dominator Tree 看保留大小GC Roots 路径最终定性。顺序错了也没关系MAT 这几个视图之间可以互相跳转右键对象的任意一处都能看到下一次分析的入口。4. 让 MAT 跑得动大堆文件内存参数与 4 个必调配置4.1 MAT 自身的 JVM 堆为什么必须调MAT 本身是 Eclipse 平台上的一个富客户端程序它启动在自己的 JVM 里。分析堆转储时MAT 会把 hprof 文件中的对象装载成内部数据结构并建立从对象到类、到引用链的索引。这个过程非常吃内存分析一个 4GB 的堆转储MAT 自身堆至少需要 4GB有时会需要 8GB 甚至更多。很多人在这一步翻车双击 MAT 后窗口打开界面上提示 Java heap space进度条走到一半就挂掉。原因很简单MAT 安装后的默认内存上限一般是 1GB这个值对日常小型分析够用对生产环境动辄几 GB 的堆转储来说完全不够。所以拿到大文件第一步不是急着开而是先改 MAT 自身的内存参数。4.2 修改 MemoryAnalyzer.iniXmx 与 Xms 的推荐值找到 MAT 安装目录下的MemoryAnalyzer.ini文件这是 Eclipse 启动器的配置入口。用文本编辑器打开文件前半段长得像下面这样重点改-Xmx和-Xms-startup plugins/org.eclipse.equinox.launcher_1.x.x.x.jar --launcher.appendVmargs -vmargs -Xms1024m -Xmx8192m逻辑说明-startup后面的 jar 是 Eclipse 启动器不同版本文件名里的版本号不一样不要改动。--launcher.appendVmargs是一个标记告诉 Eclipse 从这里开始是 JVM 参数。-Xms 设置初始堆大小-Xmx 设置最大堆大小。-Xms不一定要改但因为 MAT 在打开大文件时内存会迅速增长初始堆太小会让它在分析过程中反复扩容拖慢速度所以建议设置成最大堆的一半左右。参数说明-Xmx8192m表示最大堆 8GB具体数值按本机物理内存调整。实际经验是让它比你分析的堆转储文件大 50%~100% 比较稳妥。堆转储文件是压缩后的条目MAT 内部索引有可能比原始文件还要大预留少了依然会 OOM。改完后重启 MAT在菜单栏Help - About - Installation Details - Configuration里能看到-Xmx是否生效。还有一种情况改了-Xmx后仍然报错可能你是 32 位系统或 32 位 JVM。32 位 JVM 最大堆上限在 1.5GB 左右8GB 设置不会生效即使生效也会内存映射失败。解决办法直接换用 64 位 JVM 的安装包安装 MAT这一步无法通过参数调整绕过。4.3 大文件分析前的 4 个必调配置项内存参数改完后还有几个环境层面的配置容易被忽略。这些不是写在 ini 里的参数而是分析前的准备动作我总结成 4 个必查项。第一把 .hprof 文件放到本地磁盘。MAT 分析时需要随机读取文件并创建索引如果文件放在 NFS 网络盘上单条数据的读取延迟会放大几十毫秒整个分析过程可能从几分钟拖到一小时。所以先拷贝到本地 SSD再执行分析。第二关闭杀毒软件的实时扫描。.hprof 文件动辄几个 GB杀毒软件在后台扫描会持续占用磁盘 IOMAT 索引时频繁读文件两者互相争抢导致界面假死。做法是把堆转储所在目录加入扫描排除列表分析完成后再恢复扫描。第三预留至少两倍于转储文件大小的空闲内存。这里的空闲内存是指操作系统可用内存因为 MAT 自身堆加上索引缓存会同时存在于物理内存里。如果本机内存只有 16GB要分析一个 8GB 的 dump 就会很极限建议先关掉无关的程序或者干脆用 32GB 内存的机器。第四避免在分析机器上同时运行大堆 Java 程序。例如你本机还跑着一个 -Xmx8g 的应用再开一个 -Xmx8g 的 MAT操作系统就会进入频繁换页MAT 会表现出各种离奇报错如 disk full 或 unable to create native thread。排查前先把其他 Java 进程停掉或者换机器。把上面四条做一遍绝大多数分析场景能顺利跑完。如果还是失败回到 4.2 继续调大 -Xmx同时确认机器 jvm 架构而不是怀疑 MAT 工具本身的问题。5. 避开 5 个常见分析误区现象、原因与解决方式5.1 坑一OOM 之后手动 dump 太晚分析对象已经不准现象服务报 OOM运维人员等到事件发生后半小时才用 jmap 导出堆转储打开后却找不到明显的对象堆积泄漏线索断了。原因OOM 发生时内存已经耗尽JVM 部分对象可能正在被清理而你手动导出的那一刻堆的情况和崩溃时并不一致。如果用了jmap -dump:live还会先触发 Full GC把许多潜在的泄漏对象直接回收掉导致“证据消失”。解决最重要的是让 JVM 在 OOM 瞬间自动生成快照启动参数提前加上-XX:HeapDumpOnOutOfMemoryError。如果事后才意识到没加这个参数可以尝试手动 dump但分析结论要谨慎优先找“稳定增长”的对象而不是找“那一刻最多”的对象。另外可以用监控工具把堆使用率曲线和 dump 时间对齐确认 dump 时刻与 OOM 时刻的偏差。5.2 坑二MAT 自身堆设得比文件还小现象双击 MAT 打开一个 4GB 堆转储进度条卡在 20% 然后弹出对话框“Java heap space”或“OutOfMemoryError”连主界面都没完整显示。原因MAT 默认启动堆上限太小常见是 1GB而分析一个几 GB 的堆转储需要约 1.5 到 2 倍文件大小的堆空间。解决按第 4 章方式修改MemoryAnalyzer.ini中的-Xmx比如设置成-Xmx8g。修改后确认 JVM 是 64 位。如果是 Windows 下的 32 位系统堆上限约 1.5GB那再怎么调也无效。还有一个容易被忽略的点-Xmx必须写在--launcher.appendVmargs之后如果写在了-vmargs之前Eclipse 可能把它当成程序参数忽略导致不生效。5.3 坑三高版本 MAT 打不开老 JDK 的堆转储或解析列表为空现象用最新版 MAT 打开一个老项目 JDK 1.6 导出的 hprof 文件直接报错 “Unsupported HPROF version” 或者打开成功但 Histogram 里只有几个类。原因HPROF 格式随 JDK 版本发生过变化MAT 对旧格式的兼容性虽然一直在维护但个别旧版本 JDK 导出的文件头可能超出新版本的解析范围导致识别失败。解析列表为空则常见于 MAT 版本和 hprof 的字节序约定不一致。解决优先使用与生产 JVM 相同年代的 MAT 版本比如 JDK 1.8 对应用 MAT 1.10 左右的版本JDK 11 以上可以用较新的 MAT。如果必须用新版本尝试用 JDK 自带的jhat打开一次确认文件本身没有损坏再用 MAT 打开。事后反思最好在项目启动参数里固定 JDK 版本并让分析工具和运行环境保持同步升级避免临时找兼容版本。5.4 坑四直方图里最大的类不是根因只是被缓存持有的结果现象在 Histogram 中看到java.lang.String占内存最大于是顺着它找代码却没有发现在哪里 new 了这么多字符串。原因String 本身是 Java 程序里使用最频繁的类缓存、日志输出、JSON 序列化都会产生大量 String 实例。这些 String 本身不是问题真正的问题是背后那个静态的HashMap或ConcurrentHashMap一直在增涨把字符串当键保留。直方图只告诉你哪个类实例多不告诉你谁持有它们。解决回到 Dominator Tree按 Retained Heap 排序你会看到真正占大头的是那个 HashMap 或它的数组。右键选择Path to GC Roots - exclude weak references就能找出是哪个静态字段或线程上下文缓存把它拽住了。记住一个原则先找集合容器再找集合的持有者不要盯着 String。5.5 坑五jmap -dump:live 造成 Full GC 停顿误判服务 hang现象执行 jmap 后线上服务接口响应突然变慢监控显示请求堆积有些项目直接判定进程“卡死”并重启。原因-dump:live在导出前会触发一次 Full GC。堆越大Full GC 暂停时间越长十几 GB 的堆暂停几十秒并不稀奇。这期间所有工作线程都阻塞从外部看就是服务没有响应。解决生产环境导出用不带 live 的命令即jmap -dump:formatb,file...它只做快照复制不主动触发 Full GC。如果仍然担心快照过程对性能影响可以先摘流量或者在低峰期执行。另外在启动参数里提前配置好 OOM 自动导出避免线上应急时再做 jmap。如果你已经执行了 live 导出版本也别急着重启等 Full GC 结束后观察是否恢复必要时用 GC 日志确认停顿时间。6. 进阶用 OQL 与批处理脚本让分析报告可重复生成MAT 里有一个容易被低估的功能OQLObject Query Language。它用类似 SQL 的语法直接查询堆对象适合在已知某个类名需要调查时快速圈定对象集合。比如想找到所有长度超过 1000 的 String可以这样查SELECT * FROM java.lang.String s WHERE s.value.length 1000逻辑说明SELECT *表示返回完整对象FROM java.lang.String s是遍历堆中的所有 String 实例WHERE s.value.length 1000过滤出 value 数组较长的字符串。这个查询在分析缓存溢出的场景很有用能快速定位异常数据。参数说明value是 String 内部的 char[] 字段名OQL 语法直接访问对象字段length是数组属性返回结果右边会显示对象的引用可以直接右键Path to GC Roots。OQL 还可以配合定时任务做自动化分析。常见做法是监控到堆使用率超过阈值时自动导出堆转储然后调用 MAT 自带的命令行批处理脚本生成报告最后把报告归档。虽然 MAT 是图形化工具但安装目录下提供了 headless 入口能让你不打开界面就完成分析。我一般会把这一套命令放在脚本里遇到线上大堆问题先自动分析一轮人工再复核关键引用链效率会高很多。真正上手后你会发现MAT 的杀手锏并不在于那一个视图而是视图之间环环相扣的交叉验证。从直方图到支配树从支配树到 GC Roots最终定位到具体的类、集合与空间区域。这个过程熟悉后一次 OOM 排查时间能从半天缩短到半小时。如果你只记住一条那就记这条先把-XX:HeapDumpOnOutOfMemoryError埋到所有 Java 服务里再把 MAT 的-Xmx调到堆转储文件的两倍。没有这几个前置动作再熟练的分析技巧也救不了现场。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑