尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MAT分析hprof堆文件:Java OOM排查与内存泄漏定位实战

MAT分析hprof堆文件:Java OOM排查与内存泄漏定位实战 简介MATMemory Analyzer Tool是Eclipse基金会出品的Java堆内存分析工具可解析JVM生成的hprof文件这种标准格式记录了对象分配、存活状态与引用关系常用于诊断内存泄漏、内存占用过高等问题。压缩包内共2000个文件大小约75.44MB其中以1891个HTML帮助文档和183个JAR运行库为主体文档覆盖了工具使用手册、OQL语法说明和故障排查指引另有217张PNG截图与109个GIF动画演示操作流程并配有xml、properties等配置资源便于离线查看和直接安装使用。工具内置支配树、泄漏嫌疑人报告、Shallow/Retained Heap对比、哈希表视图以及OQL自定义查询等模块支持对比多份快照能够协助开发者理清对象生命周期、定位异常引用来源。目前已有6612人学习下载适合正在排查JVM内存异常、需要快速分析堆转储文件的初中级Java开发者同时也可作为性能调优人员的即用参考工具。1. MAT 不是 MATLABhprof 堆转储分析的入口与适用人群一个典型场景凌晨 3 点批量服务 OOM值班同事重启后一切正常第二天你连“到底谁吃掉了内存”都说不清。没有堆转储文件JVM 只剩一个被重启的事实。最靠谱的事后取证就是在事发前留一份 hprof 堆文件再用 MAT 分析 hprof 文件把对象图和引用链一页页翻出来。这篇写的是这条链路的完整操作怎么导出 hprof、怎么用 Memory Analyzer 打开、从哪几个视图找根因以及哪些坑会让整个取证白干。适合正在做 Java 后端调优、排查 OOM或者想把“重启大法”换成可复现证据的开发者。顺带一个边界如果你要找的是 matlab 的 .mat 矩阵处理那不在这篇范围。2. 拿到事发“现场”导出 hprof 的三种方式与参数取舍2.1 jmap 手动导出live 参数会改变现场jmap 是最直接的导出手段适合进程还活着、你正好在场的情况。一条命令把整个堆复制到文件里jmap -dump:formatb,file/data/heapdump/order-service-$(date %Y%m%d%H%M).hprof pidformatb表示输出二进制 hprof这是 MAT 最省解析开销的格式file指定落盘路径建议放在独立磁盘因为文件体积和当前堆大小基本相当末尾的 pid 通过jps -l或ps -ef | grep java找。执行前看一眼磁盘剩余空间df -h /data/heapdump堆 4G 时 hprof 通常也是 3~5G写满磁盘会让应用直接雪崩。这里最需要注意的是那个live参数网上很多命令写成-dump:live,formatb意思是“先做一次 Full GC再导出存活对象”。听起来能少导出很多东西实际上它把现场洗白了WeakReference、SoftReference 缓存会在 GC 后大量消失不可达对象更是直接被清掉你看到的堆是“垃圾分类后”的干净版本而不是 OOM 前的真实状态。排查泄漏恰恰需要看是谁在持有不该持有的对象所以日常分析我一般不加 live除非你明确只想看存活对象的分布。2.2 换用 jcmd不触发 Full GC也能导出完整堆JDK 8 以后更推荐用 jcmd 替代 jmap因为 jcmd 的GC.heap_dump做的事更单纯直接复制堆对象图不先触发垃圾回收。命令格式jcmd pid GC.heap_dump /data/heapdump/order-service-$(date %Y%m%d%H%M).hprof注意 jcmd 的路径参数是纯路径不支持file前缀。执行后它会打印一行Heap dump file created没看到这行就是没成功。相比 jmapjcmd 稳住现场的能力更好对低峰期之外的业务影响更小这一点在压测环境里对比 jmap 能明显感知到差异。不过这两个工具有一个共性副作用导出全量堆的过程需要把对象图序列化到磁盘期间 JVM 的 STW 时间会明显变长。4G 堆导一次 hprof业务接口 P99 涨个几百毫秒很常见所以生产上一定要挑低峰期做。如果条件允许最稳妥的做法是先把节点从负载均衡摘掉再执行 dump这也是我做线上排查时的默认动作。2.3 给 JVM 配上“后悔药”OOM 时自动留下 hprof人不可能永远在场进程也不一定撑得到你敲命令。更可靠的做法是把导出配置写进启动参数让 JVM 在抛出 OutOfMemoryError 时自己留一份现场java -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/heapdump \ -Xmx4g -jar order-service.jarHeapDumpOnOutOfMemoryError是开关HeapDumpPath是目录需要提前建好并保证 Java 进程有写权限。配上之后任何一次 OOM包括线程分配失败、元空间溢出都会在事发瞬间生成 hprof 文件文件名默认带 pid。设置后要验证一次最简单的办法是写个死循环向 List 里塞对象限着堆大小跑一次确认文件真能生成出来。这个“后悔药”配置成本极低但多数线上服务没配等到真的反复重启时才发现日志里什么都查不到。磁盘预留同样要做hprof 的典型大小约等于-Xmx的当前使用量HeapDumpPath所在分区至少留一个堆大小的余量。OOM 自动导出的 hprof 和 jmap/jcmd 手动导出的 hprof 格式一致都能被 MAT 打开。区别在于触发时机OOM 时的堆通常已经处于“垃圾堆积到极限”的状态指向性最强手动导出的堆则取决于你按下命令的那一刻业务在干什么如果时间点踩不准后面分析起来会很吃力。2.4 打开之前先校验文件头、时间戳与大小拿到 hprof 先不要急着拖进 MAT用两个命令快速确认文件的“出生信息”file /data/heapdump/order-service-20250602-030012.hprof du -sh /data/heapdump/order-service-20250602-030012.hproffile会输出Java heap dump data一类的识别结果如果输出的是data或ASCII text说明文件不完整或干脆写错了du -sh用来确认文件大小和当时堆的体量是否匹配。一个 4G 堆导出来只有 200M 的文件大概率是导出过程中出了问题或者你拿到的是一份加了live之后的“阉割版”。hprof 文件本身也有时间戳信息可以和你的事故记录对一下如果文件的生成时间和 OOM 发生时刻差了几个小时这份堆可能来自另一次无关波动分析出来只会误导方向。3. 用 MAT 读入大堆文件启动配置与 Overview 首读3.1 eclipse mat 下载与内存参数Xmx 设不好加载即崩关键词“eclipse mat 下载”对应的就是 Eclipse Memory Analyzer 这个桌面工具。官方提供 Windows、macOS、Linux 三个平台的压缩包下载后解压即可运行不需要安装。打开 bin 目录下的MemoryAnalyzer.exeWindows或MemoryAnalyzerLinux/macOS你看到的是一个基于 Eclipse 框架的图形界面。但直接双击运行去打开一个大 hprof大概率会翻车。MAT 解析 hprof 时会把整个对象图读进自己的堆里并建立索引它的默认堆上限只有 1024m一个 2G 的 hprof 就能让它直接报 OutOfMemoryError。启动前要改它的配置文件MemoryAnalyzer.inivim MemoryAnalyzer.ini-startup plugins/org.eclipse.equinox.launcher_*.jar --launcher.library plugins/org.eclipse.equinox.launcher.*.e4_*.dll -vmargs -Xms4096m -Xmx12288m-Xmx给多少我的经验是至少等于 hprof 文件大小的 1.5 倍最好到 2 倍。比如一个 6G 的堆转储-Xmx12g起步如果文件 10G 而你的机器只有 16G 内存就要考虑在解析时关闭自动生成报告或者换更大内存的机器来分析。-Xms设成和-Xmx一样避免运行过程中反复扩容触发 GC解析速度明显更稳定。提示机器内存不够时不要硬开大堆文件。常见做法是在 MAT 打开文件的对话框里勾选后台解析选项并调低解析器可用的内存上限但分析速度会慢很多。更省事的方案是把大 hprof 拿到有足够内存的机器上再分析。3.2 打开文件后的 Overview先看 Details 再看 Actions用 MAT 打开 hprof 文件是通过File → Open Heap Dump选择文件。解析过程需要一点时间窗口底部有进度条类越多越慢一个 4G 堆往往要一两分钟。解析完成后默认落在 Overview 页这是整个分析工作的指挥台。Overview 上半部分是 Actions 区放着一排功能入口Histogram、Dominator Tree、Leak Suspects、Top Components 等。下半部分是 Details 区显示Total heap size、Classes、Objects、Class Loader等总量指标。这些 mat 数据无疑是第一步要读的总量是否接近-Xmx上限、类数量是不是异常多、对象数量有没有到千万级。比如一个平时 2G 的服务hprof 里显示总量 3.8G那说明泄漏确实发生过接下来去做进一步定位才有意义。打开过程中如果看到 Leak Suspects 报告自动弹出不要急着下结论那只是 MAT 根据支配树做的“猜测”。先回到 Overview按顺序手工走下一节里面的三个视图比直接信任自动报告靠谱很多。3.3 Histogram 直方图的列浅堆和保留堆别读反Histogram直方图是按类聚合的对象统计表也是新手最容易看明白、老手最容易读错的一个视图。表里有四个关键列列名含义使用注意Class Name类名内部类用$连接按前缀能看出模块归属比如com.yourcompany.*Objects该类的实例数数量大不等于内存大要看下一列Shallow Heap浅堆对象自身占用的字节数不含它引用的其他对象Retained Heap保留堆对象被回收后能释放的总字节数这是定位内存黑洞的主力列分析时第一件事就是点 Retained Heap 列头做降序排列看排在最前面的类是谁。真实的业务问题往往体现为一个基础类型或者容器类排在最前比如byte[]的 Retained Heap 巨大说明有大量的字节数组被缓存java.lang.String数量上了百万说明字符串被批量驻留。直方图回答的是“哪些类型的对象占了多少内存”但回答不了“是谁引用了它们”所以 Histogram 只能定位到类级别再往下就要进 Dominator Tree 和 Path to GC Roots。4. 定位内存黑洞Leak Suspects、支配树与 GC Roots 路径4.1 Leak Suspects 先给结论但结论只能当线索MAT 打开 hprof 后会自动生成一份 Leak Suspects 报告它的输出形式是一段文字某个类加载器下的一类对象占据了整体堆的 94%并附带一串可疑栈。这份报告的优势是能快速给你一个起点但它只依据支配树做启发式判断经常犯“把结果当原因”的错。比如它可能告诉你“一个java.util.HashMap$Node数组占了 85% 的堆”真正的问题却是业务代码里有一个 Map 一直往里 put 不清理这个HashMap$Node[]只是被撑大的容器不是泄漏源。所以我的习惯是Leak Suspects 只用来决定从哪棵子树开始看后面的判断必须自己做。它指到了哪个类就去 Histogram 里定位那个类再继续进支配树展开。不要看到报告第一屏就写进工单结论MAT 的这份报告本质是把工具层的推断推给你查不查得出来还得靠你顺着引用链走一遍。4.2 Dominator Tree 看结构在树上找业务对象而不是基础类型Dominator Tree支配树是 Histogram 之外最值得花时间的视图。它把对象按引用关系组织成一棵树每个节点的 Retained Heap 表示“这棵子树被整体回收后能释放的内存”。这个视图的最大价值是它打破了类聚类的视角把问题还原到对象图结构上。操作路径是Overview → Dominator Tree打开后同样按 Retained Heap 降序排列。排查时第一眼要找的不是byte[]或Object[]而是排在最前面的“业务对象”——比如你们的订单缓存类、Session 管理类、批处理上下文类。找到之后逐层展开它的子树看它的子节点里哪些字段在膨胀。一个常见的模式是业务对象本身浅堆很小可能只有几百字节但它的 Retained Heap 是几百兆因为它的成员变量挂着一整个已经失控的集合。这一步要注意支配树上每个父子关系只代表“我引用了它”不代表“我是它内存变大的原因”。展开到叶子节点之前最好不要下结论真正要命的分支往往是展开两三层之后浮出来的某个具体集合字段。4.3 Path to GC Roots把引用链拉出来排除弱引用再看选中支配树里可疑的那个业务对象右键选择Path to GC Roots可以看到这个对象是怎么被 GC Roots 一路引用住的。这里有两个选项经常让人犹豫with all references显示所有引用路径包括软引用、弱引用、虚引用信息最全但噪声大。exclude weak/soft/phantom references只保留强引用链这是最接近“真凶”的读法。排查持有性泄漏的时候我一般直接用排除弱引用的选项。原因很简单弱引用的对象本应该在 GC 时被回收它还在堆里说明另有强引用路径在保它排掉弱引用之后剩下的那条强引用链就是最老实的线索。比如一条典型的路径会显示Thread → ThreadLocalMap → Entry → value → ArrayList → ...看到这里基本可以锁定是线程池里的 Task 对象持有了一个大集合没在 finally 里清理。在引用链视图里还有一个容易被忽略的功能右键可以Merge Shortest Paths to GC Roots它会把成千上万条引用路径折叠成少数几条最短路径特别适合处理那种“对象数量多、但根因集中在同一个线程”的问题。用最短路径功能前先加一个筛选把java.lang.ref.*、sun.misc.*等系统类路径折叠掉人肉看的干扰会小很多。说白了GC Roots 分析就是把“黑匣子”里的引用关系可视化能不能定位到根因取决于你能否在折叠后的路径里认出一个业务类名。5. 避坑hprof 分析最容易翻车的 5 个细节5.1 加载大堆直接 OOM 或假死Xmx 没调就开文件现象MAT 打开 hprof 后进度条走到一半界面卡死控制台报OutOfMemoryError或者直接退出无响应。 原因MAT 自身的堆上限还停留在默认的 1024m大 hprof 的对象图索引塞爆了它的堆。 解决按第 3 章的方法提前修改MemoryAnalyzer.ini把-Xmx调到 hprof 文件大小的 1.52 倍。已经在卡死状态的直接强制结束进程改完配置重新打开不要等。额外提醒解析大文件时 MAT 会在 hprof 同目录生成索引文件磁盘不足也会造成假死先看磁盘。5.2 解析报Unknown HPROF versionJDK 和 MAT 版本跨代了现象加载 hprof 时弹错Unknown HPROF version或Unsupported format文件根本打不开。 原因高版本 JDK比如 JDK 17、21导出的 hprof 元数据和老版本 MAT 识别逻辑不匹配老 MAT 认不出新文件头。 解决升级 MAT 到较新版本新版解析器兼容 JDK 8 到 21 的主流导出格式。如果升级后仍然打不开用file命令确认文件是不是完整的 HPROF排除文件截断。另一个相关的注意点是线上环境往往是 JDK 8分析机器上装的是新版 MAT这种组合通常没问题反过来就大概率翻车。5.3-dump:live把现场洗白分析结果永远对不上事故现象用jmap -dump:live导出的 hprof 打开后文件体积明显偏小Leak Suspects 报告里找不到事故时刻最可疑的对象。 原因live参数触发了一次 Full GC弱引用、软引用、不可达对象被清理后才做 dump分析的是“打扫过的房间”不是“案发现场”。 解决线上导出用jmap -dump:formatb或jcmd pid GC.heap_dump不带 live。如果只有带 live 的历史文件至少要知道这份 hprof 缺失了什么所有被缓存和临时持有的非强引用对象都不会出现结论只能停留在存活态层面排查缓存类泄漏基本无效。这一条是很多人的血泪教训导出命令敲下去之前一定要想清楚。5.4 支配树最大节点不等于泄漏源把结果当原因现象Dominator Tree 顶部是一个HashMap$Node[]或Object[]Retained Heap 占比极高于是你锁定它就是元凶。 原因数组和容器是被业务逻辑撑大的“容器”它们排第一说明有业务代码往里面加了太多数据但容器本身不会自己长大。把容器当根因去改方向就偏了。 解决沿着支配树往下展开找到持有这个容器的业务类。通常你会看到某条分支上的自定义对象一层层引用到这个容器根因是那个对象没有在业务结束时释放集合引用。排查建议把展开层级控制在 23 层先找com.yourcompany.*命名的类再看它的成员变量比死盯基础类型高效得多。5.5 集群环境拿错节点分析了“别人”的堆现象hprof 打开了总量、对象分布都正常看不出任何泄漏迹象和 OOM 事故完全对不上。 原因线上服务多副本负载均衡你拿到的 dump 来自另一个健康节点或者来自某次发布前的旧实例OOM 节点已经被弹性伸缩摘除文件没留下来。 解决采集阶段就要确认主机名、pod 名和 pid。在 dump 前执行hostname或看jinfo里的启动参数确认目标和事故节点一致。导出的文件命名带上主机名和时间比如order-svc-node03-20250602-030012.hprof避免事后拿了文件不知道是谁的。这个问题在容器化环境特别容易踩节点被替换后 hprof 会随 pod 一起消失所以在预案里就要把 dump 文件放持久卷。6. 进阶用 OQL 补刀把堆里对象直接审问出来图形界面几个视图用得再熟也总有“我想按条件过滤对象”的需求这时候 MAT 内置的 OQL 比任何按钮都好使。OQL 是一种专门查堆的查询语言语法类似 SQL 但字段名换成对象属性。打开方式是Toolbar → Open Query或者菜单里的 Query Browser。最常用的一个查询是找大字节数组排查 I/O 缓存和文件读取类问题的利器SELECT b, b.retainedHeapSize FROM byte[] b WHERE b.length 1048576 ORDER BY b.retainedHeapSize DESC这段 OQL 的意思是扫描所有byte[]对象筛出长度超过 1MB 的按保留堆降序排列。retainedHeapSize是 MAT 对对象计算的保留堆大小伪属性和直方图里的 Retained Heap 口径一致。查出来的结果双击就能跳到对象引用页直接看谁持有了这个大数组。对 4G 以上的堆执行这个查询可能要等十几秒属正常现象。另一个常用案例是按业务类过滤找出某个缓存对象的所有实例SELECT c, c.retainedHeapSize FROM INSTANCEOF com.example.order.OrderCache ORDER BY c.retainedHeapSize DESCINSTANCEOF后面跟带包名的全限定类名支持子类匹配。这个查询用来回答一个很实际的问题“这个类到底被实例化了几次每个实例挂了多少数据”排查订单缓存在多次操作后膨胀的场景比千篇一律地看直方图直观得多。OQL 支持LIMIT关键字结果太多时记得加上避免把界面卡住。最后说一个验证习惯单份 hprof 只能看静态快照要确认泄漏还在持续发生我会在压测前、压测中、恢复后各导一份 hprof三份直方图的 Retained Heap 排序放一起对比看哪个类的对象数量只涨不跌。涨的才是泄漏涨完又回去的是正常水位。我现在排查任何可疑泄漏都会先把这三份现场留齐再开 MAT不然就是在给玄学找理由。希望帮到你。本文还有配套的精品资源点击获取
返回列表