ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

MAT 分析 hprof 文件实战:堆内存泄漏排查与 OOM 定位指南 简介MATMemory Analyzer Tool是Eclipse基金会推出的Java堆内存分析工具专为诊断内存泄漏、内存占用过高及对象生命周期问题而设计适合有一定JVM基础的Java开发与性能调优人员使用。本资源围绕其核心能力展开即解析JVM生成的hprof内存剖析文件帮助定位问题对象与类。压缩包共约2000个文件以html文档、png与gif图示、jar依赖包为主辅以xml、dita、css、properties等配置与说明文件整体约75.44MB结构完整便于离线查阅与工具运行。目前已有6613人学习下载。内容涵盖对象分配与生存时间、支配树、大型对象集、泄漏嫌疑人报告、Shallow Heap与Retained Heap对比、多次快照对比分析、OQL自定义查询以及线程堆栈等视图可帮助读者系统掌握从生成hprof到逐步排查内存问题的完整思路是优化Java应用性能的实用辅助资料。1. MAT 工具分析 hprof 文件从一次堆内存告警说起线上服务在凌晨两点触发堆内存告警运维把一份 2.3 GB 的 hprof 文件丢过来说“你看看吧”。这时候真正能救命的不是重启而是 MAT——Eclipse Memory Analyzer。它专门用来分析 hprof 文件也就是 JVM 在 OOM 前后 dump 出来的堆快照。hprof 文件本质是某一时刻整个 Java 堆的对象图谱谁引用了谁、每个对象占多大、哪些类加载器还活着全在里面。MAT 做的事就是把这团黑匣子拆开算出支配树、找出泄漏嫌疑、定位到具体代码。适合谁后端开发、SRE、做性能优化的同学尤其是被“内存缓慢上涨最后 OOM”折磨过的人。这篇笔记按“先跑通、再读懂、再定位、最后避坑”的顺序讲命令和参数都能直接抄。2. 把 hprof 喂给 MAT三种打开方式与内存配置2.1 先搞清楚 hprof 是怎么来的没有 hprof 文件MAT 再强也没用。常见获取方式有三种选哪种取决于你是想“事后复盘”还是“主动抓现场”。第一种是 OOM 自动 dump启动参数加-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/JVM 在抛 OutOfMemoryError 时自动落盘。这是最省事的做法缺点是只能拿到崩溃那一刻进程已经快不行了堆里可能全是垃圾。第二种是运行时手动触发用jmap -dump:live,formatb,file/data/dump/heap.hprof pid。注意live参数加了它只 dump 存活对象会先触发一次 Full GC文件更小、更干净不加则连待回收对象一起 dump文件大但保留了完整现场。生产环境我一般先不加 live因为 Full GC 本身可能把现场“洗掉”。第三种是通过 JMX 或诊断命令触发适合容器里没有 jmap 的场景。容器内 PID 为 1 时 jmap 经常报错这时用jcmd pid GC.heap_dump /data/dump/heap.hprof更稳。# 查看当前 JVM 进程确认 PID jps -l # 方式一dump 全部对象保留完整现场文件偏大 jmap -dump:formatb,file/data/dump/heap_all.hprof 12345 # 方式二只 dump 存活对象先触发 Full GC文件小 jmap -dump:live,formatb,file/data/dump/heap_live.hprof 12345 # 容器内 jmap 不可用时改用 jcmd jcmd 12345 GC.heap_dump /data/dump/heap_jcmd.hprof参数说明formatb表示二进制格式MAT 只认这个file后面是绝对路径目录必须存在且进程有写权限否则会静默失败——这是血泪经验dump 目录权限不对时命令返回成功但文件是 0 字节。live的取舍上面说过了抓泄漏优先不加抓“到底谁在占内存”可以加。2.2 MAT 本体怎么装、内存怎么给MAT 有两个形态桌面版和独立命令行版。桌面版适合交互式排查命令行版适合在服务器上批量生成报告。桌面版下载后解压即用但有个关键点MAT 自己也是 Java 程序分析大 hprof 时它自己会吃掉大量堆内存。默认配置下打开 2 GB 以上的 hprof 大概率直接 OOM报Java heap space。解决办法是改 MAT 安装目录下的MemoryAnalyzer.ini把-Xmx调大。经验值hprof 文件大小的 1.5 到 2 倍。比如 2 GB 的 hprof给 MAT 配 4 GB 堆。# MemoryAnalyzer.ini 关键几行 -startup plugins/org.eclipse.equinox.launcher_xxx.jar -vmargs -Xmx4096m -Xms1024m -XX:UseG1GC逻辑说明-Xmx4096m是 MAT 进程自己的最大堆不是被分析应用的堆两者别搞混。-XX:UseG1GC能让大堆下的 GC 停顿更平滑交互时不至于卡死。如果你的机器内存只有 8 GB分析 4 GB 的 hprof 会很吃力建议换 16 GB 以上的机器或者用命令行版配合-xmx参数在服务器上跑。2.3 命令行版生成报告服务器上不开图形界面生产服务器通常没有图形界面这时候用 MAT 的 ParseHeapDump 命令行工具直接产出 HTML 报告和索引文件下载回本地看。# 进入 MAT 安装目录 cd /opt/mat # 解析 hprof 并生成泄漏嫌疑报告和支配树 ./ParseHeapDump.sh /data/dump/heap_all.hprof \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview \ org.eclipse.mat.api:top_components # 输出文件会生成在 hprof 同目录下形如 heap_all_Leak_Suspects.zip参数说明org.eclipse.mat.api:suspects生成泄漏嫌疑报告这是最常用的overview生成总览top_components生成大对象组件报告。三个可以一起给也可以只给 suspects。生成的 zip 解压后是 HTML用浏览器打开即可。注意命令行版同样受-Xmx限制如果报内存不足编辑同目录下的ParseHeapDump.sh找到-Xmx那行调大。3. 读懂 MAT 的四张核心视图支配树、直方图、引用链、泄漏嫌疑3.1 支配树Dominator Tree谁真正占着内存打开 hprof 后 MAT 默认展示 Overview但真正干活的是 Dominator Tree。它和普通对象树不一样普通树里一个对象被多个地方引用会重复计算支配树按“支配关系”算——如果删掉 A 就能释放 B那 A 支配 B。所以支配树里每个节点的 Retained Heap 才是它真正“压住”的内存。看支配树的方法按 Retained Heap 降序排从最大的往下点。常见现象是排第一的是某个byte[]或HashMap$Node[]展开后能看到它被谁持有。比如一个ConcurrentHashMap占了 1.2 GB展开发现 key 是用户 IDvalue 是会话对象那基本就是会话没清理。Class Name | Shallow Heap | Retained Heap java.util.concurrent.ConcurrentHashMap | 48 | 1,234,567,890 |- java.util.concurrent.ConcurrentHashMap$Node[] | 800,000,000 | 1,200,000,000 | |- ... 大量 NodeShallow Heap 是对象自身大小Retained Heap 是它被回收后能释放的总大小。排查泄漏只看 Retained HeapShallow Heap 大的对象不一定有问题比如一个巨大的数组可能只是缓存。3.2 直方图Histogram按类看数量快速找异常Histogram 按类聚合显示每个类的实例数和 Shallow Heap 总量。它的价值在于快速发现“某个类的实例数多得不正常”。比如正常情况Session对象几百个结果有 200 万个那不用想泄漏点就在这。操作上在 Histogram 里右键某个类选Merge Shortest Paths to GC Roots排除弱引用、软引用后就能看到从 GC Root 到这个类的引用路径。这条路径就是“谁在一直抓着它不放”。Class Name | Objects | Shallow Heap com.example.Session | 2,013,442 | 320,000,000 java.lang.String | 8,900,000 | 712,000,000 char[] | 9,100,000 | 1,456,000,000注意 String 和 char[] 数量大通常是结果不是原因它们被别的对象引用着。要顺着引用链往上找真正的持有者。3.3 引用链与 GC Roots定位“谁在抓着不放”GC Roots 是可达性分析的起点线程栈局部变量、静态变量、JNI 引用、常量等。一个对象如果从 GC Root 可达就不会被回收。MAT 的Path to GC Roots功能就是展示这条路径。排查时右键可疑对象选Path to GC Roots→exclude weak/soft references。排除弱引用和软引用是因为它们不阻止回收留着会干扰判断。看到路径后重点看两类一是静态集合static Map、static List二是长生命周期线程的 ThreadLocal。这两类是泄漏重灾区。Thread http-nio-8080-exec-12 |- java.lang.ThreadLocal$ThreadLocalMap | |- java.lang.ThreadLocal$ThreadLocalMap$Entry | | |- com.example.UserContext -- 泄漏对象上面这条链说明线程池里的线程一直活着它的 ThreadLocalMap 里存着 UserContext如果请求结束后没调remove()这个对象就永远跟着线程线程不销毁它就不释放。线程池场景下这就是典型的 ThreadLocal 泄漏。3.4 泄漏嫌疑报告Leak SuspectsMAT 的自动诊断Leak Suspects 是 MAT 自动分析后给出的“嫌疑清单”它会指出几块大内存和可能的泄漏点。新手可以直接从这份报告入手但别全信——它只是基于启发式规则有时候会把正常的大缓存标成嫌疑。报告里每个 suspect 会给出占用大小、可能的持有者、一段描述。点进去能看到具体的对象和引用链。我的习惯是先看 Leak Suspects 缩小范围再用 Dominator Tree 和 Path to GC Roots 验证两者对上了才动手改代码。4. 避坑与排查分析 hprof 时最容易翻车的五件事4.1 现象MAT 打开 hprof 报 “Java heap space”文件根本打不开原因MAT 自身堆内存不够不是被分析应用的问题。默认-Xmx往往只有 1 GB 左右遇到大文件直接崩。解决改MemoryAnalyzer.ini里的-Xmx给到 hprof 大小的 1.5 到 2 倍。如果机器内存不够用命令行版在服务器上跑或者先用jmap -dump:live抓一份小的。另外可以开启 MAT 的“按需加载”选项在 Preferences 里勾选Keep unreachable objects的反向设置减少内存占用。4.2 现象dump 出来的 hprof 文件是 0 字节或只有几 KB原因dump 目录权限不对或者磁盘满了。jmap 在写文件失败时不一定报错命令返回 0 但文件是空的。解决dump 前先df -h看磁盘再确认进程用户对目标目录有写权限。容器场景注意挂载卷的权限。养成习惯dump 完立刻ls -lh看文件大小几百 MB 到几 GB 才正常。4.3 现象MAT 里看到的对象数量和实际业务对不上怀疑分析错了原因dump 时加了live参数触发了 Full GC待回收对象被清掉了或者 dump 的是不同时间点的快照业务状态已经变了。解决抓泄漏现场时不要加live保留完整对象。如果必须对比抓两份不同时间点的 hprof用 MAT 的Compare Basket功能对比对象增长这样能看出哪些对象在持续增加比单份快照更准。4.4 现象Path to GC Roots 里全是弱引用/软引用找不到真正的持有者原因默认没排除弱引用和软引用它们不阻止回收但会出现在路径里干扰判断。解决右键选Path to GC Roots时明确选exclude weak references和exclude soft references。如果还是找不到检查是不是被finalize队列或者 JNI 全局引用持有这两类比较隐蔽。4.5 现象定位到某个大 Map但代码里找不到对应的清理逻辑原因可能是第三方框架内部的缓存比如 MyBatis 的本地缓存、Spring 的某个单例缓存或者连接池没配上限。解决看这个 Map 所属的类加载器如果是框架的类去查框架文档的缓存配置。常见的有 MyBatis 一级缓存SqlSession 级别通常没问题和二级缓存Mapper 级别要配 eviction。连接池如 HikariCP 的maximumPoolSize设太大也会导致连接对象堆积。定位到框架后调配置比改代码更靠谱。5. 进阶技巧用 OQL 和对比分析把泄漏钉死到这一步常规视图已经能解决八成问题。剩下两成需要更精确的手段我常用的是 OQL 查询和双快照对比。OQL 是 MAT 内置的对象查询语言语法类似 SQL能直接按条件筛对象。比如想找出所有 size 超过 1000 的 ArrayListSELECT * FROM java.util.ArrayList a WHERE a.size 1000在 MAT 的 OQL 面板里执行结果会列出所有匹配对象右键可以直接看引用链。再比如找所有 value 为 null 但 key 还在的 HashMap Entry或者找某个包下所有实例数超过阈值的类。OQL 的价值在于把“肉眼翻支配树”变成“条件筛选”大堆里尤其省时间。双快照对比更直接间隔一段时间抓两份 hprof用 MAT 打开两份通过Compare Basket把 Histogram 加进去对比。看Objects和Shallow Heap的增量增长最快的类基本就是泄漏源。这个方法能排除“本来就大但不增长”的正常缓存只盯住“持续膨胀”的部分。我一般间隔 10 到 30 分钟抓第二份太短看不出趋势太长可能已经 OOM。还有一个容易忽略的点MAT 的索引文件。第一次打开 hprof 会生成.index文件之后打开快很多。如果 hprof 要反复分析别删索引。另外 MAT 支持把多个 hprof 放在一个snapshot历史里方便回溯。最后说个习惯每次排查完把结论记下来——哪个类、哪条引用链、什么原因、怎么改的。下次遇到类似现象直接搜记录比重新翻支配树快得多。内存泄漏的花样就那么几类静态集合、ThreadLocal、缓存无上限、监听器没注销、连接没关闭。见得多了打开 MAT 看几眼就能猜到方向。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进