资讯详情

Eclipse MAT堆转储分析:从OOM定位到内存泄漏排查实战

📅 2026/10/27 11:03:27 | 华诺云谱 👁 阅读
Eclipse MAT堆转储分析:从OOM定位到内存泄漏排查实战
简介面向Java开发与性能调优人员的Eclipse Memory Analyzer ToolMAT独立运行包聚焦堆转储分析与内存泄漏排查适合需要定位大对象、引用链及内存占用异常的中高级开发者。压缩包共5442个文件、约133.56MB以html帮助文档、png与gif界面图示、jar插件、class及xml与properties配置为主并含少量hprof示例转储和可执行程序可离线搭建完整分析环境。已有370人学习下载。借助Leak Suspects报告、Dominator Tree、Histogram、重复对象检测与多快照对比等功能读者能够梳理对象支配关系、追踪引用链、评估Shallow与Retained Heap并对照内置文档理解各视图判读方法为线上内存问题复盘和JVM调优提供可复用的分析依据。1. 内存日志翻车现场为什么日志里搜不到的那行代码MAT 一眼就指出来线上每隔几小时必 OOM 一次应用日志里只有一行java.lang.OutOfMemoryError: Java heap space堆栈每次落在不同的业务方法上翻遍日志也指不出到底谁在涨。这种场景下Eclipse MAT 这类堆转储日志分析工具才真正派上用场它读的不是文本日志而是 JVM 崩溃瞬间打出来的 hprof 快照把上百万个对象、引用关系、GC Roots 离线还原成可检索的结构化视图。要先说清一个容易误解的点MAT 不负责 grep 文本日志。搜「mat 日志分析」的人一半是想找内存快照分析工具一半是想找能读 GC 日志的工具MAT 属于前者。它把二进制堆快照当成一份结构化日志来读回答的是「谁占着内存、谁引用着它、为什么 GC 回收不掉」这三个问题。适合谁写 Java 后端、跑中间件、维护 Android 客户端的同学以及要定期做内存体检的 SRE。不适合只想看应用日志里 ERROR 关键字的人那种需求用文本检索就够了。2. 把 hprof 当日志读MAT 的索引机制与四张核心视图2.1 hprof 里存的是什么对象图、GC Roots 与类元信息一个 hprof 文件本质是 JVM 堆在某一时刻的序列化快照。它包含几类信息所有类的元数据类名、父类、字段名与类型、所有存活对象的实例数据对象地址、所属类、字段值、对象之间的引用边谁指向谁以及 GC Roots——线程栈上的局部变量、静态字段、JNI 引用、常量池引用等。理解「引用边是有方向的」这件事是后面所有分析的前提A 引用 B不代表 B 是垃圾要看 A 能不能从 GC Root 走到。hprof 是二进制格式字段按类型紧凑排列字符串和数组单独成段。它的体积通常接近堆的存活数据量压缩率不高一个 8G 堆打出来的 dump 五六 G 很常见。这也是为什么把 dump 从生产拖回本地这一步经常先卡住本地磁盘、传输带宽、MAT 自身内存三个环节任何一个不够都白搭。判断一份 dump 有没有价值看两点。第一它是 live 还是 all——只含存活对象的 dump 用来定位泄漏更干净含不可达对象的 dump 适合分析「对象为什么还没被回收」之外的场景但噪音大。第二它和 GC 日志的时间点能不能对齐——同一时刻的 GC 日志、线程栈、dump 三件套放在一起才能解释清楚「为什么此刻堆是这个样子」。我一般要求抓 dump 时顺手记下时间戳和一次jstack后面省很多事。2.2 索引为什么第一次打开慢得像死机第二次几秒就出来MAT 打开 hprof 不是把整个文件读进内存而是先建索引。索引过程扫描一遍全文件记录每个对象的文件偏移、类结构、出边和入边关系然后把结果写成一堆.index文件放在 hprof 同目录下。第一次打开慢是因为它在做全量扫描第二次打开快是因为直接读索引。这解释了几个高频现象。索引文件丢了或者 hprof 被改名、移动MAT 会重新建索引用户感知就是「怎么又要等二十分钟」。索引文件和 hprof 放在网络盘或者只读目录上会失败或者极慢——索引必须可写。多个 MAT 实例同时打开同一个 hprof可能互相踩索引文件出现「文件损坏」的报错。MAT 自身的堆内存是另一个关卡。索引、对象图、视图缓存都占 MAT 进程的堆经验值是给 MAT 分配的-Xmx至少要 hprof 大小的一半最好接近或超过它。一个 6G 的 dump 开 2G 的 MAT表现不是干脆的 OOM而是长时间无响应、点一下卡十秒、最后抛OutOfMemoryError: Java heap space。这种情况先把MemoryAnalyzer.ini里的-Xmx调上去再考虑换机器。顺带一个判断索引完成后MAT 顶部会显示 dump 的堆大小和对象总数。如果这个数字和你预期的堆使用量差得离谱比如线上堆用了 8Gdump 里只有 1G那基本是抓取姿势有问题抓的是 live 且刚做完 Full GC别急着往下分析。2.3 四张核心视图与两个 size 的分工打开 dump 后真正要用的视图就那么几个搞清楚它们各自的职责比记菜单重要。Histogram直方图按类聚合给出每个类的实例数、浅堆Shallow Heap总量。它回答「哪类对象数量异常」是最快的入口但不给引用关系。Dominator Tree支配树基于对象图的支配关系构建如果从 GC Root 出发到某个对象的所有路径都必须经过 X那么 X 支配该对象。支配树里节点的 Retained Size 表示「把这个对象干掉能连带释放多少内存」。它回答「谁真正压着内存」是定位大头的核心视图。Path to GC Roots到 GC Roots 的引用链反过来走从某个对象出发向上找是谁引用着它。它回答「为什么它回收不掉」是判定泄漏的最后一步。Top Consumers 和 Leak Suspects 是基于上述能力自动生成的汇总报告前者按类、类加载器、包聚合大头后者用启发式规则猜泄漏点。Leak Suspects 适合当入口不适合当结论。两个 size 必须分清。Shallow Size 是对象自身占的内存不含它引用的对象一个HashMap实例的浅堆可能只有几十字节。Retained Size 是移除该对象后能释放的总量包含它独占支配的所有对象。看直方图时看到byte[]占了 4G那只说明「有大量字节数组」说明不了是谁的看支配树时看到某个业务对象 Retained Size 3.8G才真正指到了嫌疑人。3. 取数与导入从 jcmd 抓快照到 ParseHeapDump 命令行索引3.1 三条取数路径以及它们对生产的不同代价抓 dump 有几种常见方式代价差别很大选错就是一次事故。第一种是jcmd的GC.heap_dump通过 attach 机制触发JDK 自带容器里只要和目标进程在同一 PID 命名空间就能用。第二种是jmap -dump功能类似但老版本里live会强制一次 Full GC停顿随堆大小线性增长几十 G 的堆能停几秒到十几秒。第三种是 JVM 自己在 OOM 时打适合事后复盘。# 先用 jcmd 确认目标进程避免抓错 PID jcmd -l # 抓取堆快照默认只导出存活对象加 -all 才包含不可达对象 # 文件名带上时间戳方便和 GC 日志、线程栈对齐 jcmd 12345 GC.heap_dump /data/dump/app-$(date %m%d-%H%M).hprof # 需要连不可达对象一起看时体积会明显变大 jcmd 12345 GC.heap_dump -all /data/dump/app-full.hprofGC.heap_dump后面第一个参数是目标 JVM 的 pid路径必须是目标进程有写权限的位置容器里指向挂载卷否则进程一重启 dump 就跟着没了。默认行为只导出存活对象这对泄漏分析通常是好事——不可达对象本来就会被回收留在 dump 里只是噪音。-all适合排查「对象明明没被引用却迟迟不回收」这类怪问题。代价要提前算清楚dump 过程中 JVM 会进入安全点堆越大停得越久业务侧感知就是接口超时。生产上我一般选流量低谷、确认有摘流预案再动手并且优先挑单实例而不是全集群一起抓。3.2 让 JVM 自己吐快照OOM 参数与容器里的路径陷阱比起到现场抓更省事的是让 JVM 在 OOM 那一刻自己打。参数不多但每一项都容易漏。# 启动参数OOM 时自动 dump并指定目录 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump # 可选dump 完成后直接退出避免进程半死不活继续接流量 -XX:ExitOnOutOfMemoryErrorHeapDumpPath指向目录时文件名由 JVM 自动生成通常带 pid 和时间戳指向具体文件路径时按固定名写。ExitOnOutOfMemoryError是个取舍加上的好处是快速失败、让编排层重启实例坏处是丢掉现场不加的话进程可能处于「还能响应但全是错」的僵死状态反而更难处理。我倾向加上前提是 dump 目录可靠。容器环境最容易踩的坑是路径。dump 写进容器可写层容器一重建文件就没了写进挂载卷但卷空间不够dump 写一半失败还会把节点磁盘写满连带影响同节点其他实例。上线前确认三件事挂载卷存在且可写、剩余空间大于堆上限、有清理策略比如保留最近三份。这三件事任何一件没做OOM 自动 dump 就等于没配。另外注意HeapDumpPath目录如果不存在JVM 不会帮你创建dump 直接失败日志里只有一行警告很容易被忽略。3.3 导入与命令行索引ParseHeapDump 的用法小 dump 直接开图形界面就行启动 MAT选择打开堆转储向导里会让你选报告类型Leak Suspects / Component / 直接进直方图。大 dump 建议先走命令行把耗时的索引和报告生成丢给服务器本地只看结果。# 进入 MAT 安装目录命令行入口是 ParseHeapDump.shWindows 下是 .bat cd /opt/mat # 依次生成泄漏嫌疑报告、总览、Top Consumers # 输出是 hprof 同目录下的若干 zip解压后用浏览器打开 index.html ./ParseHeapDump.sh /data/dump/app.hprof \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview \ org.eclipse.mat.api:top_componentsParseHeapDump.sh第一个参数是 hprof 路径后面跟一个或多个报告 id。suspects出的是泄漏嫌疑报告overview是总体概况加饼图top_components按类、类加载器、包三个维度列大头。生成的 zip 是自包含的 HTML可以拷给不方便装工具的同事看。这条命令同样吃内存读的是 MAT 目录下MemoryAnalyzer.ini里的-Xmx配置。跑之前先把这个值调大不然索引到一半就挂。经验做法是在服务器上串行跑别开多个进程并发处理多个 dump——每个进程都要独立的内存机器扛不住还容易互相抢 IO。4. 定位泄漏三条线直方图、支配树、Path to GC Roots 的实操顺序4.1 第一步永远是直方图先看谁的数量不正常拿到 dump 别急着看 Leak Suspects先开直方图按 Shallow Heap 排序扫一眼 Top 20。这一步的目的是建立「基线感」一个正常业务应用的堆里byte[]、char[]、String、HashMap$Node、各类业务 DTO 应该各占多少比例心里有数之后异常才刺眼。几种典型信号。某个业务类的实例数达到几十万甚至上百万而它的语义是「一次请求创建一个」那基本能确定是集合只增不减。byte[]数量正常但单个特别大多半是某个缓冲或者一次性读取整个文件。java.lang.ref.Finalizer数量异常增长说明有对象重写了finalize且回收不及时。ClassLoader实例数持续增长指向热部署或者动态代理生成类的问题。直方图有个开关很关键默认会把java.lang.Object之类的基础类折叠也会把一些内部实现类归并。分析业务代码时我一般关掉「按类加载器分组」的干扰只留业务包前缀做过滤视图立刻干净。还有个小技巧直方图上右键某个类可以直接跳到「该类的所有实例」再对实例列表按 Retained Size 排序就能看到「这个类里最大的那几个对象分别是谁」。这一步经常直接指向一个具体的缓存 Map 或者一个大 List比在支配树里翻半天快。4.2 支配树与 Retained Size把账单算到具体对象头上直方图告诉你「哪类多」支配树告诉你「谁压着内存」。打开 Dominator Tree按 Retained Size 降序最上面几个节点通常就是答案。读支配树要注意层级含义。根节点下面是若干直接被 GC Root 支配的对象比如主线程、系统类加载器、某个静态字段持有的单例。往下展开才到业务对象。如果你看到一个业务对象 Retained Size 2G展开它下面挂着三个HashMap那就是这三张表在吃内存回到代码里搜这三个字段的写入点即可。Retained Size 的数值有一个反直觉的地方同一批对象可能被多个父节点共享谁都不「独占」它们。这时候支配树上的数字会显得偏小因为共享部分被算到了共同祖先头上。遇到这种情况别死磕数字改看「对象数量」和「引用链」比纠结几百兆更有效。另一个常见误判是把Thread当成泄漏源。支配树里某个线程的 Retained Size 特别大往往只是因为这个线程的栈上或者 ThreadLocal 里挂着大对象真凶是那个对象而不是线程本身。展开线程往下看一层通常就能定位到具体的 ThreadLocal 变量或者局部变量。分析完记得做减法验证把怀疑的那个集合清空或者加个上限重新压测抓一份 dump对比同一个类的 Retained Size 有没有降下来。这一步是区分「看起来像」和「确实是」的唯一办法跳过它很容易改错地方。4.3 Path to GC Roots排除弱引用后剩下的是真凶确定了嫌疑对象最后一步是看它为什么活着。选中对象右键打开到 GC Roots 的引用路径MAT 会列出从根到该对象的所有可达链。关键是排除项。默认视图里混着大量弱引用、软引用、虚引用和Finalizer引用这些引用本身不阻止 GC 回收混在里面会把水搅浑。右键菜单里选择排除弱引用/软引用后剩下的路径才是真正「钉住」对象的强引用链。我见过不少分析绕了半天最后发现只是一条WeakHashMap的边白忙一场。读路径时从下往上看最靠近对象的那条边是直接持有者往上是它的持有者一直到 GC Root。常见的强引用链形态有几类——静态Map字段直接持有业务对象集合线程池里的ThreadLocalMap持有大对象且线程长期存活监听器列表只注册不注销连接池或者缓冲池内部队列堆积。每一类的修法都不一样所以必须看到具体的字段名而不是停在「某个 HashMap 持有」这种粒度。路径特别长的时候用「合并最短路径」更清晰MAT 会把多条路径合并展示去重之后主干就出来了。如果路径经过类加载器还要留意是不是动态生成的类或者热部署残留导致的整类加载器泄漏这种问题在 Path to GC Roots 里表现为一条指向ClassLoader的链修法是把加载器本身释放掉。4.4 OQL 与导出把怀疑变成可验证的查询图形界面点久了会想要可重复的东西OQL 就是 MAT 里的查询语言语法接近 SQL能直接对堆里的对象做条件筛选。-- 找出 size 超过 5000 的 HashMap快速定位只增不减的缓存表 SELECT h FROM java.util.HashMap h WHERE h.size 5000 -- 找出超长字符串配合业务前缀过滤能查到拼接失控的日志字段 SELECT s FROM java.lang.String s WHERE s.value.length 10000 -- 按类名找实例类名要写全限定名 SELECT o FROM com.example.order.OrderCache o第一行查的是HashMap实例本身的size字段MAT 能直接访问对象字段不用反射语法。第二行的s.value是字符串内部的字符数组length是数组长度属性不同 JDK 上这个字段可能是byte[]写不出来的时候先在直方图里点进String看字段名再改。第三行按全限定类名取实例适合确认某个单例缓存里到底塞了多少条。查询结果可以导出成 CSV后面用脚本处理就自由了。导出直方图 CSV 之后做一次过滤排序是很多人的常规操作import csv # MAT 直方图导出的 CSV 表头Class Name, Objects, Shallow Heap rows [] with open(histogram.csv, newline, encodingutf-8) as f: for r in csv.DictReader(f): rows.append((r[Class Name], int(r[Objects]), int(r[Shallow Heap] or 0))) # 只留业务包过滤掉 JDK 和框架的噪音 biz [r for r in rows if r[0].startswith(com.example.)] # 按浅堆降序取前 20输出 MB 便于阅读 for name, objs, shallow in sorted(biz, keylambda x: x[2], reverseTrue)[:20]: print(f{name:60s} {objs:10d} {shallow / 1024 / 1024:10.2f} MB)这段脚本干的事很朴素把 MAT 导出的直方图 CSV 读进来按包前缀过滤按浅堆排序打印。Class Name和Objects、Shallow Heap是 MAT 导出时的固定列名如果导出的是支配树 CSV列名会变成Class Name、Objects、Shallow Heap、Retained Heap脚本里加上第四列就能按 Retained Size 排。参数上唯一要改的是包前缀换成自己的业务包名。5. 避坑与排查五个把 MAT 用废的瞬间5.1 生产直接 jmap -dump 没评估停顿现象抓 dump 的瞬间监控上一片接口超时告警持续时间从几百毫秒到十几秒不等。原因jmap -dump:live会先触发一次 Full GC 再写文件停顿时间和堆大小、对象数量正相关即使是jcmd GC.heap_dump写文件阶段也要进安全点几十 G 的堆照样停。很多人是在排查故障时顺手抓的等于在伤口上再补一刀。解决抓之前先看堆上限和当前使用量估算停顿选低峰期优先挑单个实例能靠 OOM 自动 dump 的就别手动抓。确实要在高峰期抓提前和上游打招呼并准备摘流。另外别在同一个实例上连续抓多份第二次的停顿可能比第一次更长。5.2 本地打开大 dump 直接假死现象双击 hprofMAT 启动画面转圈十几分钟没反应最后弹OutOfMemoryError: Java heap space或者干脆卡死不动。原因MAT 的索引和视图缓存都吃自己的堆默认-Xmx往往只有 1G 出头。一个 6G 的 dump 用 1G 去开不是慢是根本不够。解决改 MAT 安装目录下MemoryAnalyzer.ini的-Xmx给到 hprof 大小的一半以上机器内存允许就直接给到等量。仍然开不动就走命令行ParseHeapDump.sh在服务器上生成报告本地只看 HTML。另一个土办法是先只生成suspects和top_components两个报告不打开完整对象图内存压力小很多。5.3 索引文件和 hprof 不同步现象同一份 dump昨天打开正常今天打开报错说索引损坏或者视图里的对象数量对不上。原因索引文件.index存在 hprof 同目录由第一次打开时生成。hprof 被改名、移动、重新拷贝覆盖索引却没跟着更新MAT 就按旧索引去读新文件。索引写到一半被中断磁盘满、进程被杀也会留下半成品。放在网络盘或者多人共享目录上两个 MAT 同时写同一份索引损坏概率更高。解决认准「一份 hprof 配一套索引」改名或者移动时把.index一起处理或者干脆删掉让它重建。分析用的 dump 放在本地 SSD别放共享盘。重建索引很慢但比重试十次快。5.4 把 Leak Suspects 报告当结论现象按报告里指出的位置改了代码上线后内存照涨不误。原因泄漏嫌疑报告是启发式规则生成的它按 Retained Size 排序猜几个大头不理解业务语义。共享对象、缓存预热、批量任务中间态都容易被它误判。反过来真正的缓慢泄漏因为单次占比小也可能被它整个漏掉。解决报告只当入口结论必须落到引用链上。看到嫌疑人先展开 Path to GC Roots排除弱引用确认是强引用链钉住的再看这个对象的生命周期是否符合预期。最后用「改完再抓一份对比」来验证。5.5 抓的是活对象却想分析「为什么没被回收」现象dump 里搜不到某个本该存在的对象怀疑抓错了。原因默认的GC.heap_dump和jmap -dump:live只导出存活对象不可达对象在 dump 前就没了。想分析「对象处于什么状态、为什么卡住不回收」需要包含不可达对象的完整快照。解决需要全量时加-alljmap侧对应去掉live只写formatb,file...。代价是文件变大、噪音变多、索引更慢所以只在明确要查回收问题时才用。多数泄漏分析用 live 快照更干净。6. 进阶玩法headless 批量出报告以及两份快照的对比单次分析解决的是「现在出了什么问题」日常做内存体检需要的是「这段时间有没有变坏」。我的做法是把 MAT 当批处理工具用定期抓 dump、定期出报告、定期对比。cd /opt/mat # 串行处理目录下所有 dump每份都生成嫌疑报告和总览 # 注意别并发跑每个进程都要独立内存机器扛不住 for f in /data/dump/*.hprof; do echo $f ./ParseHeapDump.sh $f \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview done这段脚本没有魔法关键是「串行」和「固定报告集」。报告 id 固定下来产物的目录结构和文件名就可预期后面写脚本汇总时不用猜。生成的 zip 解压后是一堆 HTML其中index.html是入口pages/下是按类聚合的明细页。真正有价值的是对比。两份 dump 各导一份直方图 CSV用同一套过滤规则取业务包看增量和绝对量对比项快照 A快照 B判断业务类实例总数42 万118 万明显上涨重点看某缓存类 Retained Size210 MB1.9 GB只增不减基本确诊线程数180182正常波动排除ClassLoader 数量96340疑似动态类或热部署残留表格里第三行是典型的「排除项」线程数稳定说明不是线程泄漏注意力可以集中到集合类。第四行的 ClassLoader 数量翻了几倍通常意味着有代码在运行时生成类且没有释放加载器这种问题在单份 dump 里很难看出来两份一对比就露馅。我一般会把对比结果落成一句话结论比如「缓存类 Retained Size 一周涨了 9 倍实例数与请求量线性相关怀疑没有淘汰策略」再附上两份 CSV 的文件名。这样即使后面换人接手也能顺着这条线复查不用从零再翻一遍堆。血泪教训是别等出事才第一次打开 MAT。工具的生疏程度会在最紧张的时候被放大——找不到 Path to GC Roots 的排除选项、忘了改-Xmx、把索引文件拷来拷去每一个都足以让排查多花两小时。从那以后我每次上线新服务都会在压测环境抓一份基线 dump 存着出问题时拿它和现场 dump 一比八九不离十。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑