☰
Eclipse MAT堆转储分析:OOM内存泄漏定位与GC日志交叉验证
2026/10/27 19:21:17 网站建设 项目流程

简介:Eclipse MAT(Memory Analyzer Tool)堆内存分析工具面向Java开发与性能调优人员,用于定位内存泄漏、分析JVM内存结构与对象引用链。压缩包共收录5442个文件,约133.56MB,以html帮助文档、png与gif界面截图、jar插件包、xml与properties配置、dll与exe运行组件为主,另有hprof堆转储示例、bat批处理脚本及log运行记录,基本还原一套可独立部署的MAT运行与文档环境。工具功能涵盖Leak Suspects泄漏嫌疑报告、支配树与直方图视图、重复对象检测、引用链追踪、Shallow Heap与Retained Heap碎片分析,以及多份快照对比;配套文档对这些视图的含义与判读方法均有展开说明。已有370人学习下载,适合希望在真实heap dump上练习定位大对象、梳理引用关系、排查循环引用的开发者按图索骥地查阅与实践。

1. 先找到 OOM 现场的记录:Eclipse MAT 分析的对象是什么

凌晨被告警叫起来,堆内存打满,重启之后服务恢复正常,日志里除了几行 OutOfMemoryError 什么线索都没留下——这种场景下继续往下翻业务日志基本是白费力气,真正能定位问题的证据在堆转储文件里。Eclipse MAT(Memory Analyzer Tool)解析的是这份堆转储:它把几百兆到几 G 的 hprof 解析成可检索的对象图,让你看到 OOM 那一刻到底是谁占着内存不放、是从哪条引用链走过来的。它解决的重点在于「哪条引用让对象活过了它该活的年纪」,而不是「日志里报了什么错」。适合需要处理 Java 服务内存问题的后端开发、SRE 和中间件维护人员,前提是你手上有一份能打开的堆转储,并且愿意花半小时理解支配树和 Retained Heap 的统计口径。标题里它被叫作「日志分析工具」,实际用起来更像堆转储这类诊断文件的读取工具——这个差别,决定了后面每一步该怎么走。

2. 环境与取数:让 MAT 能打开文件、让 dump 有分析价值

很多人第一次用 Eclipse MAT 就失败了,失败点通常不在分析思路上,而在两个前置环节:MAT 自身的堆开得太小,或者拿到的 dump 文件本身没有分析价值。这一章把这两件事钉死,后面的分析才有意义。

2.1 独立版与插件版的选择,以及 -Xmx 必须先调

Eclipse MAT 有两个分发形态。独立版是一个免安装的 RCP 程序,解压就能跑;插件版装进 Eclipse IDE,好处是分析堆的时候能顺手看源码和调用栈,坏处是 IDE 本身也吃内存,大 dump 一开,两个进程抢内存,最后谁都不好受。我一般固定用独立版,只做堆分析时不去启动整个 IDE。

真正容易出问题的是 MemoryAnalyzer.ini 里的-Xmx。MAT 解析 hprof 时会把对象索引和元数据载入自己的 JVM,默认值往往只有 1024m,遇到 2G 以上的 dump 直接卡在 Parsing 阶段然后抛 OutOfMemoryError,界面上看起来像是「文件损坏」,其实是分析器自己被撑死了。

# 解压后先别急着双击启动,先检查并调整解析器自己的堆上限 cd /opt/mat grep -n 'Xmx' MemoryAnalyzer.ini # 经验值:dump 文件大小的 1.5~2 倍,8G 内存机器处理 4G dump 比较从容 sed -i 's/^-Xmx.*/-Xmx8g/' MemoryAnalyzer.ini # 如果机器上装了多个 JDK,显式指定一个,避免用错版本 grep -n '^-vm' MemoryAnalyzer.ini

这段操作的逻辑很直白:MemoryAnalyzer.ini是 Eclipse 系程序的启动配置文件,-Xmx控制的是 MAT 进程自身的堆。把 -Xmx 改到 dump 大小的 1.5 倍是个常用的起点,低于 1 倍基本没有成功可能。-vm那一行要写在-vmargs之前,指向具体的 java 可执行文件,不要指向目录。另外注意 ini 里参数是一行一个,-vm和路径必须分成两行,写在同一行会失效。

版本兼容也值得单独说一句。新版本 JDK 导出的 hprof 格式会有变化,用很老的 MAT 去打开,症状是解析到一半报 unsupported version 或者直接空指针。同一份 dump 在同事机器上打得开、你这里打不开,先对比 MAT 版本,不要急着怀疑文件。

2.2 用 jcmd 和 jmap 拿到一份能用的 hprof

堆转储有两个来源:出问题时手工导出,或者靠 JVM 参数在 OOM 瞬间自动导出。后者是更值得配置的做法,因为 OOM 现场转瞬即逝,人工介入往往已经错过了峰值。

# 1. 先看进程的堆参数和当前占用,判断值不值得导 jcmd 12345 VM.flags | tr ' ' '\n' | grep -E 'HeapSize|HeapDump' jcmd 12345 GC.heap_info # 2. 导出全量堆转储(包含已死对象),STW 时间相对短 jcmd 12345 GC.heap_dump /data/dump/heap-$(date +%Y%m%d%H%M).hprof # 3. 只要存活对象:会先触发一次 Full GC,生产上慎用 jmap -dump:live,format=b,file=/data/dump/heap-live.hprof 12345 # 4. 不想进 MAT 也能先扫一眼谁占得多,按字节数倒序 jmap -histo:live 12345 | sort -k3 -nr | head -20

第 2 条用的是jcmd GC.heap_dump,等价的旧写法是jmap -dump:format=b,file=...,两者导出的内容基本一致。关键差别在live参数:带上 live 会先做一次 Full GC,只保留可达对象,dump 更小更干净,但代价是一次完整的 STW,几十 G 的堆可能卡住数秒到数十秒,对延迟敏感的服务是实打实的伤害。不带 live 拿到的是导出瞬间的全部对象,包含大量已经该被回收的垃圾,分析时会被它们的体积误导——这一点在第 5 章的避坑里还会再提。

第 4 条是个省事的小技巧:jmap -histo的输出列依次是序号、实例数、字节数和类名,所以按第 3 列倒序排就能得到一份粗粒度的占用排行。它不能替代 MAT,但在决定要不要导 dump 之前,几十秒就能给出方向。

自动导出的参数建议在服务启动时就加上:

# 放在启动脚本的 JAVA_OPTS 里,OOM 时自动落盘,这是最便宜的后悔药 JAVA_OPTS="$JAVA_OPTS -XX:+HeapDumpOnOutOfMemoryError" JAVA_OPTS="$JAVA_OPTS -XX:HeapDumpPath=/data/dump" JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError"

HeapDumpPath指向的目录要预留至少两倍堆大小的空间,dump 文件本身接近堆的实际使用量,写满磁盘导致进程二次崩溃的例子不少。ExitOnOutOfMemoryError让进程在 OOM 后主动退出交给编排系统拉起,比带着一个残缺的堆继续对外服务要安全。

2.3 Overview 首屏:先看四个数字,再决定往哪走

文件打开后第一屏是 Overview,信息密度很高但真正需要先读的只有几项:Details 区域里的 Size(dump 文件大小)、Classes(加载的类数量)、Objects(对象数量)、Class Loader(类加载器数量),右侧是按 Retained Heap 排的饼图,下方是 Leak Suspects 和 Top Consumers 的入口。

判断顺序我一般是这样:先看 Classes。正常业务服务几万到十几万算常见,如果这个数字到几十万甚至上百万,优先怀疑动态生成类——反射、字节码增强、脚本引擎、反复热部署都会推高它,对应的下游问题是 Metaspace 而不是堆。再看 Class Loader 数量,成百上千个加载器通常意味着有容器或插件机制在反复创建加载器而没有释放,这是典型的类加载器泄漏。最后看 Objects,它只反映对象个数,跟内存占用没有线性关系,一个人人都在用的短生命周期对象池也能堆出很高的数字,不要被它带偏。

Overview 里还有个容易被忽略的 Unreachable Objects Histogram。它是「如果现在做一次 GC,哪些对象会被回收」的模拟视图。如果这个入口点进去发现不可达对象占了很大比例,说明这份 dump 是没带 live 参数导出的,后面所有基于 Retained Heap 的结论都要打折扣,因为你分析的是一个包含了大量本该消失的对象的快照。

提示:Overview 上的数字只用来定方向,不要据此下结论。真正的证据在 Histogram 和 Dominator Tree 里,前者回答「哪类对象多」,后者回答「谁把它们留住了」。

3. 直方图与支配树:把「谁占了内存」拆到可执行粒度

拿到一份能打开的 dump 只是起点。Eclipse MAT 里真正做判断的是两个视图:Histogram 按类聚合,回答「哪类对象占了最多内存」;Dominator Tree 按支配关系组织,回答「回收哪个对象能一次性释放最多内存」。两者的统计口径不同,混用会得出相反的结论。

3.1 Histogram 的列含义与正则过滤的正确用法

Histogram 有四列:Class Name、Objects、Shallow Heap、Retained Heap。Shallow Heap 是对象自身占的字节数,一个对象头加字段,数组则是元素总长;Retained Heap 是这个对象被回收后能连带释放的内存总量,也就是它加上所有只被它支配的对象。默认按 Shallow Heap 排序,这个默认值极具误导性——按 Shallow 排,排在前面的永远是byte[]、char[]、java.lang.String、int[]这些「载体类」,它们数量多、单体小,但从来不告诉你业务上是谁在持有。

正确的第一步是切到 Retained Heap 排序,然后在顶部的正则框里过滤包名。比如输入com\.example\.就把视图收窄到自家代码,第三方框架和 JDK 基础类型全部隐藏。这个过滤是正则不是通配符,点号要转义,.*Service这种写法才是合法的。过滤之后如果 Retained 排行里出现了你熟悉的业务类,那基本就锁定了嫌疑范围。

# 命令行下的近似替代:不打开 GUI 也能看到类的实例数和字节数排行 jmap -histo:live 12345 | awk 'NR>1 && $3 > 1048576 {printf "%-8s %-60s %s\n", $2, $4, $3}' | sort -k3 -nr | head -20

这段 awk 的逻辑是跳过表头,只保留字节数超过 1MB 的行,输出实例数、类名和字节数并按字节数倒序。它给出的是 Shallow 口径的排行,和 MAT 的 Histogram 在「谁大」这个问题上结论通常一致,但完全没有 Retained 的概念,也看不到引用链。把它当作快速初筛,真正定位还是得进 MAT。

过滤之后还有一种情况:自家包名下什么都没剩下,Retained 排行前几名全是java.util.HashMap$Node或者java.util.concurrent.ConcurrentHashMap。这说明内存被容器类持有,要进一步看这些容器的 key 是什么类型,或者直接跳到 Dominator Tree 看容器的持有者。

3.2 Dominator Tree 与 Retained Heap 的统计口径

支配关系的定义不复杂:如果从 GC Root 出发到对象 B 的所有路径都必然经过对象 A,那么 A 支配 B。Dominator Tree 就是把这层关系画成树,父节点被回收时,它支配的所有子节点都会一起消失,所以父节点的 Retained Heap 是整个子树的总和。

这个口径解释了为什么一个业务上的「缓存管理器」对象 Shallow Heap 只有几十字节,Retained Heap 却有 800MB——它自己很小,但它持有的那个 ConcurrentHashMap 以及里面的几十万个 Entry 全部被它支配。也解释了为什么 Histogram 里把某一类的 Retained Heap 加起来会超过堆总大小:同一个对象可能被多个根同时可达,在多个父节点下面被重复计入。Retained Heap 是「如果只回收这一个对象能释放多少」的估算,不是精确的归属划分,拿它做加总没有意义。

实际操作上,我习惯先在 Histogram 里定位到可疑的类名,然后右键选「Merge Shortest Paths to GC Roots」→「exclude all phantom/weak/soft references」,得到从 GC Root 到这个类的引用链。这条链就是证据链:起点是线程栈、静态变量还是 JNI 引用,中间经过哪些容器,最后落到哪个业务字段。链上每一跳都能对应到代码里的一个赋值点。

注意:分析引用链时要排除弱引用、软引用和虚引用。缓存框架大量使用 SoftReference,不排除的话会看到一堆「看起来被引用着」其实随时可回收的路径,误导性极强。MAT 的右键菜单里提供了现成的排除选项。

另外,Dominator Tree 的默认视图是聚合过的,同一个类的大量实例会被折叠成一行,展开才能看到单个对象。看单个对象时留意它的地址和 Shallow 大小,配合 Inspector 面板能看到字段值,这一步往往能直接看到业务主键、用户 ID 之类的信息,从而定位到具体的请求或数据分片。

3.3 从 Leak Suspects 报告反推业务代码

打开 dump 后 MAT 会自动跑一遍启发式分析,生成 Leak Suspects 报告。报告里每条 suspect 包含三段:问题描述、累积对象所在的支配树节点、以及从 GC Root 到累积点的最短路径。很多人只看第一段就下结论,其实第三段才是最有价值的——它给的是「谁在引用这个累积点」,是整条链上最接近业务代码的一跳。

读报告的顺序建议反过来:先看 Shortest Paths 里的引用链,找到第一个属于自家包名的类;再看这个类在代码里被谁持有、生命周期由谁管理;最后回到 Accumulated Objects 确认累积的规模是不是和 suspect 描述一致。suspect 排第一不代表它就是泄漏,一个设计上就要常驻的大缓存同样会被排到前面,区别在于它的大小是否随时间单调增长。

判断是否真泄漏,最可靠的办法是隔一段时间采两份 dump 做对比,看同一个类的实例数和 Retained Heap 是不是持续上涨。单份 dump 只能告诉你「现在谁大」,两份 dump 才能告诉你「谁在涨」。第 4 章会给出批量对比的做法。

4. OQL 与命令行:把一次性排查变成可复用脚本

GUI 点一遍能解决一次故障,下次换个人、换台机器又得重新学。Eclipse MAT 提供了两个把经验沉淀下来的入口:OQL 查询语言和 ParseHeapDump 命令行。前者让你精确地从对象图里捞出目标,后者让整套报告生成过程无人值守。

4.1 OQL 的几个实用查询

OQL 的语法接近 SQL,但操作对象是堆里的实例而不是表。它支持SELECT ... FROM 类名 别名 WHERE 条件,也能访问元数据,比如数组长度、类的加载器、对象的 Retained 大小。类名用全限定名,FROM java.lang.String s这种写法是合法的,FROM char[] c也可以。

-- 1. 捞某个业务缓存里条目数异常多的实例 SELECT * FROM com.example.cache.OrderCache c WHERE c.entryCount > 10000 -- 2. 统计每种 Map 实现的实例数量,判断容器选型是否失控 SELECT c.@clazz.name AS cls, COUNT(*) AS cnt FROM java.util.Map c GROUP BY cls -- 3. 找出所有超过 1MB 的 byte 数组,定位大对象 SELECT s FROM byte[] s WHERE s.@length > 1048576 -- 4. 从字符串池里按关键词捞缓存 key,定位是哪个业务维度在膨胀 SELECT s.toString() FROM java.lang.String s WHERE s.toString().contains("orderId")

第 1 条是最常用的形态:先确认这个类存在,再用业务字段做筛选。字段名必须和类里的实际字段一致,私有字段也能访问,不需要 getter。第 2 条用到@clazz.name这个元数据访问器,@前缀在 OQL 里表示「取对象的元信息」,@length用于数组,@retainedHeapSize用于任意对象,@clazz返回 Class 对象。第 3 条直接按数组长度过滤,比在 Histogram 里翻页快得多,尤其适合排查大文件上传、批量查询结果集这类场景。第 4 条对字符串做 contains,代价很高,必须配合足够窄的条件,否则在几十 G 的 dump 上会跑到天荒地老。

OQL 没有 LIMIT 子句,控制结果规模只能靠 WHERE 条件收窄。另外它对正则的支持有限,复杂匹配不如导出后在本地处理。查询结果可以导出成 CSV,这是把 MAT 和其他工具串起来的关键一步。

4.2 ParseHeapDump 无界面跑报告

每次出问题都开 GUI 点一遍不现实,尤其是需要在测试环境定时采集、事后回溯的场景。MAT 自带命令行入口ParseHeapDump.sh(Windows 下是.bat),可以在无图形界面的机器上直接把报告生成出来。

#!/usr/bin/env bash set -euo pipefail MAT_HOME=/opt/mat DUMP=$1 OUT_DIR=/data/report/$(basename "${DUMP%.hprof}") mkdir -p "$OUT_DIR" cp "$DUMP" "$OUT_DIR/" "$MAT_HOME/ParseHeapDump.sh" "$OUT_DIR/$(basename "$DUMP")" \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview \ org.eclipse.mat.api:top_components \ -format=zip \ -vmargs -Xmx8g ls -lh "$OUT_DIR"

脚本的关键在参数位置。第一个参数是 hprof 文件的完整路径,后面跟若干个报告标识,org.eclipse.mat.api:suspects生成泄漏嫌疑报告,overview生成概览页,top_components生成大对象排行,三个一起跑基本覆盖了初筛需求。-format=zip让输出打包成 zip,报告会生成在 hprof 同目录下,文件名和 dump 同名。-vmargs -Xmx8g是给 MAT 自身进程传参,和 GUI 模式下改 ini 是一个意思,漏掉这一项在大 dump 上同样会 OOM。

这个脚本适合挂在采集任务后面:dump 落盘后自动触发解析,报告归档到对象存储,人只需要看 zip 里的 index.html。要提醒的是命令行模式并不会比 GUI 省内存,它对同一份 dump 的解析开销是一样的,只是省了图形渲染。机器内存不够的时候,把任务排到夜间或者换一台大内存的分析机,不要指望调小 -Xmx 硬扛。

4.3 两份 dump 的差异对比

MAT 本身没有内置的 diff 功能,但 Histogram 视图支持导出 CSV,配合命令行工具就能做出增量分析。思路是把两个时间点的类级统计各导一份,按类名对齐后算差值。

# 两份 CSV 的列约定:Class,Objects,Shallow Heap,Retained Heap # 先各自抽出类名和 Shallow 列并排序,join 要求输入有序 cut -d, -f1,3 t1.csv | sort > t1.sorted cut -d, -f1,3 t2.csv | sort > t2.sorted # 按类名外连接,缺失的一侧补 0,输出类名 + t1 值 + t2 值 join -t, -a1 -a2 -e0 -o '0,1.2,2.2' t1.sorted t2.sorted \ | awk -F, '{delta=$3-$2; if (delta > 1048576) printf "%-62s %+12d\n", $1, delta}' \ | sort -k2 -nr | head -20

join的-a1 -a2表示两边都保留,即使某个类只在一侧出现;-e0表示缺失字段填 0;-o '0,1.2,2.2'指定输出格式,0是连接键,1.2和2.2分别是第一个和第二个文件的第二列。awk 里算差值并只保留增量超过 1MB 的类,最后倒序取前 20。这样得到的就是「两次采样之间谁的 Shallow Heap 涨得最多」。

选择 Shallow 而不是 Retained 做对比,是因为 Retained 在两次 dump 之间的计算会受对象图结构变化影响,同一个类可能因为支配关系变化而数值跳动,噪音比信号大。Shallow 的增长是实打实的对象变多或变大,更适合做趋势判断。确认某类确实在涨之后,再回到单份 dump 里用 Dominator Tree 看它的引用链。

5. 排查与避坑:MAT 用错比不用更费时间

这一章列的都是在实际排查里反复出现的错误。共同点是症状看起来像是「工具坏了」或者「结论就是如此」,实际上问题出在使用方式上。

5.1 取数与打开阶段的问题

现象:打开 dump 后进度条停在 Parsing,几分钟后弹 OutOfMemoryError。原因:MAT 进程自身的堆上限不够,解析索引装不下。解决:改MemoryAnalyzer.ini的-Xmx到 dump 大小的 1.5 倍以上,或者在命令行模式加-vmargs -Xmx。如果机器物理内存不够,换机器比调参有效,用 16G 内存的笔记本开 32G 的 dump 是把时间浪费在等待上。

现象:jmap -dump:live执行期间服务接口大面积超时,监控上是一条明显的毛刺。原因:live 参数会先触发一次 Full GC,几十 G 的堆上这个停顿是秒级的,加上 dump 本身的写盘时间,整体 STW 可能到十几秒。解决:优先用 OOM 自动导出的 dump;确需手工导出时去掉 live 参数,接受 dump 里包含死对象的代价,分析时用 Unreachable Objects Histogram 辅助判断。不要在高流量的生产节点上随手执行带 live 的 dump。

现象:dump 文件导出成功,但大小只有几 MB,打开后对象少得可怜。原因:导出时进程刚重启或刚做过 Full GC,堆本来就是空的;也可能是HeapDumpPath指向的目录权限不对,写了个空文件。解决:先在GC.heap_info里确认当时的堆占用,再决定导不导。一份反映不了问题的 dump,分析得再细也没用。

现象:同一份 dump,同事能打开,你打开报 unsupported version。原因:MAT 版本和导出 dump 的 JDK 版本不匹配,新版本 JDK 的 hprof 格式有调整。解决:统一团队的 MAT 版本,把它和 JDK 版本一起写进排查手册。版本不一致还会导致同一个查询的 Retained 结果有偏差,对比结论时容易各说各话。

5.2 结论层面的误判

现象:按 Shallow Heap 排序,第一屏全是byte[]和char[],改了半天代码也没降下来。原因:这些是载体类,真正的问题在于谁持有它们。解决:切到 Retained Heap 排序并按包名正则过滤到自家代码,或者直接在 Dominator Tree 里找持有者。盯着 byte[] 永远找不到业务代码。

现象:Leak Suspects 第一条指向某个缓存类,团队据此把缓存容量砍了一半,线上 QPS 掉了一截但内存没怎么降。原因:那个缓存是设计上就要常驻的,suspect 的启发式规则只看体积不看增长趋势。解决:隔 30 分钟采第二份 dump 做增量对比,只有持续上涨的类才值得动。单份 dump 定不了泄漏。

现象:Histogram 里把某几类的 Retained Heap 相加,得到的总和比堆还大,怀疑工具算错了。原因:Retained Heap 是「单独回收这个对象能释放多少」的估算,多个根同时可达的对象会被重复计入。解决:不要做加总,只看单类的排行和支配树层级。判断总量用 Overview 里的 Size。

现象:用 jmap -histo 看到的实例数,和 MAT 里 Histogram 对不上,怀疑 dump 不完整。原因:jmap -histo:live 会先做 GC,采到的本来就是 GC 之后的状态,而 dump 可能是 GC 之前的快照;两者统计口径和采样时刻都不同。解决:对比时确认采样条件一致,要么都用 live,要么都不用。跨时刻的数字对比没有意义。

6. 从堆转储回到 GC 日志:两条证据链的交叉验证

堆转储是某一瞬间的静态快照,它告诉你「谁占着内存」,但不告诉你「内存是怎么涨上来的」。GC 日志补的正是这一段:它是时间序列,能画出老年代占用的曲线、Full GC 的频率和每次回收的量。两者对上了,结论才站得住。

打开 GC 日志之前先确认参数是否齐全。JDK 9 之后统一用-Xlog,之前的版本是另一套参数:

# JDK 9+ 的写法,输出时间戳、运行时长、级别和标签 JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/data/log/gc.log:time,uptime,level,tags:filecount=5,filesize=50m" # JDK 8 及更早版本的写法 JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCDateStamps" JAVA_OPTS="$JAVA_OPTS -Xloggc:/data/log/gc.log -XX:+UseGCLogFileRotation" JAVA_OPTS="$JAVA_OPTS -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M"

filecount和filesize这两个参数很多人不加,结果是日志无限增长把磁盘写满,或者出了问题回头发现日志已经被覆盖。轮转配置在服务启动时一次性加好,事后补是补不回来的。

有了日志和 dump,接下来是交叉验证。我常用的对照关系整理成了下面这张表:

GC 日志表现对应到 MAT 里看什么典型结论
Full GC 后老年代占用几乎不下降Dominator Tree 里 Retained 最大的常驻对象真泄漏,有对象被长期持有
Full GC 频繁但每次都能回收大半Histogram 中生命周期短但体积大的类型瞬时大对象,如批量查询、大文件解析
老年代缓慢爬升,Full GC 间隔逐渐缩短两份 dump 的类级增量对比缓慢泄漏,常见于本地缓存无上限
堆没满却报 Metaspace OOMOverview 的 Classes 和 Class Loader 数量动态类生成或类加载器未释放
GC 停顿长但回收量很小Histogram 里的大数组和字符串堆本身过大或参数配置不当

举一个具体的走法。GC 日志显示老年代在 6 小时内从 30% 爬到 85%,期间 Full GC 发生了 4 次,每次回收后只降到 78%,这就是典型的「回收不动」。此时采一份 dump,在 Histogram 里按 Retained 排序并过滤自家包名,如果发现com.example.xxx.LocalCache排在第一,展开 Dominator Tree 看它下面的 Map 有多少个 Entry,再右键看引用链确认它挂在一个静态字段上——到这里证据就闭环了:静态字段持有无上限的本地缓存,GC 日志里的曲线和 dump 里的对象量能对上。

反过来,如果 GC 日志显示每次 Full GC 都能把老年代清到 40% 以下,只是频率高,那多半不是泄漏,而是对象晋升太快或者年轻代配得太小,方向应该是调参数和看分配速率,而不是去 dump 里找引用链。用错方向的排查,工具再好也救不回来。

从那以后我每次拿到 dump,都会先问一句「GC 日志在不在」,两份材料对不上的时候,我信日志的趋势、信 dump 的细节,但绝不信任何单方面的结论。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询