ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java内存排查实战:用MAT分析堆转储定位OOM根因

2026/10/5 11:13:33 拓冰建站 浏览量
Java内存排查实战:用MAT分析堆转储定位OOM根因 1. 先搞清楚MAT是什么能帮你解决什么问题做Java后端的人多多少少都遇到过OOMOutOfMemoryError或者内存缓慢上涨的问题。排查这类问题光看日志和监控曲线往往不够真正能一锤定音的是把那份Java进程的堆转储heap dump文件拉出来然后交给MATMemory Analyzer Tool分析。这工具是Eclipse基金会出的开源项目名字叫Memory Analyzer Tool经常被简称为MAT日常大家说的“用MAT看堆”“跑一下MAT”指的都是它。我知道很多人会把它和另一个大名鼎鼎的MATLAB搞混尤其是刚接触的人搜索时很容易把“mat数据”“mat文件”带偏。这里先说清楚MAT是分析Java堆内存的输入的是.hprof或.hprof.gz这类堆转储文件MATLAB是数值计算平台它读写的是二进制.mat文件。虽然缩写撞了但完全是两条技术路线。后面我还会专门展开这两者的区别。拿MAT能解决什么问题打个比方一个JVM进程的内存就像一间仓库OOM就是仓库爆仓了。监控只能告诉你仓库满了、是什么时候满的但看不出是谁霸占了货架、哪些货是应清未清的。MAT可以把这个仓库翻个底朝天哪个类对象数量最多、哪个对象占用内存最大、哪个线程持有大对象不释放、这些对象从GC Roots是怎么被引用链拉扯住的。适合谁用刚接手线上服务、第一次遇到OOM的初级开发也能用直方图和泄露疑点报告快速找到入口对JVM底层有一定理解的人则可以靠支配树、OQL做深挖。这是从事Java开发、线上稳定性保障、中间件维护的人绕不开的功课。我最初接触MAT是因为一个Spring Boot服务每两天准时Full GCgc日志和监控图都看不出哪个业务在作妖。导出堆转储之后五分钟内就定位到了一个静态HashMap持有业务缓存、只写不删。这种经历让我彻底确定MAT不是万能的但缺了它内存排查至少痛苦十倍。2. 核心功能一从堆转储到可疑对象的定位链路2.1 堆转储的生成与导入用MAT之前必须有一份堆转储文件。常见生成方式有几种jmapjmap -dump:live,formatb,file/path/heap.hprof pid这是最经典的方式。加live表示只保留存活对象文件会小很多分析也快。JVM启动参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/heap.hprof让JVM在OOM瞬间自动落盘生产环境强烈建议提前加上。容器和云平台Kubernetes诊断工具、阿里云/腾讯云的JVM诊断功能一般也能导堆。还有Arthas的heapdump命令适合不方便用jmap的容器环境。拿到.hprof之后打开MAT选择File - Open Heap Dump即可。首次打开会弹出向导让你选择报告类型通常选“Leak Suspects Report”就够了。这里有个实操建议堆转储文件最好和生产实例的JDK版本保持一致不同JDK地址布局差别可能导致解析报错。还有如果文件几百MB以上记得把MAT的MemoryAnalyzer.ini初始堆调大比如-Xmx4096m否则工具自己就先OOM了这个坑我踩过不止一次。导入后MAT默认会自动跑一遍Leak Suspects Report。它会通过启发式规则找出最像内存泄漏的可疑对象给出“一个类的实例在某个线程内被不断增长地持有”“一个对象占用了总堆的xx%”之类的结论。对新手来说这是最快的起点但我的建议是不要把结论当圣旨它只是一个索引最终判定要靠后续操作。2.2 Histogram第一眼看清对象与数组的规模导入堆转储后工具栏上最显眼的入口就是“Histogram”。它展示的是按类汇总的对象数量Objects和占用空间Shallow Heap、Retained Heap。界面可以按任意列排序我一般先按Retained Heap由大到小排这样最占内存的类立刻浮出水面。看到byte[]、char[]、Object[]这类数组类排在最前面不用紧张这是常态。重点看的是某个业务类是否异常地出现在Top N或者某些字符串数组的数量远超预期。举个例子之前排查一个报表服务直方图里java.lang.String对象数量达到上千万Retained Heap占了堆的三分之一。点进去按值分组后发现大量完全相同的SQL模板字符串被重复缓存。问题从“内存涨了”缩小到“字符串重复”这个定位过程快得惊人。需要注意Histogram的单位默认是字节显示的是Shallow Heap和Retained Heap两列新手经常不知道选哪列。记着一个原则判断“这个类总体占多少内存”看Retained Heap判断“这个类本身字段占多少内存”看Shallow Heap。如果想排除无效对象可以把Regex过滤条件用起来比如输入包名前缀com.example只显示自己业务包下的类而不是被JDK集合类淹没。2.3 Dominator Tree找出真正支配内存的对象Histogram是“按类”的视角Dominator Tree则是“按对象”的视角它基于一个关键概念对象之间的“支配关系”。理解它我习惯用部门结构来类比——只有撤掉某个主管整个小组才会解散那这个主管就“支配”了这个小组的内存。在对象引用图里如果从GC Roots出发到达对象B必须经过对象A那么称A支配B。Dominator Tree就是把这些支配关系组织成一棵树树里越靠近根的节点越值得怀疑。因此当你想知道“到底是谁把一个巨大对象集拽在内存里”应去Dominator Tree而不是Histogram。在树中右键一个对象选择Path to GC Roots - with all references就能看到从根到这个对象的完整引用链。我之前排查过一个在线客服服务直方图显示byte[]很大但看不出业务来源在支配树里点开最大的byte[]往上追溯发现它被一个ChannelOutboundBuffer持有再往上是一个Netty的NioSocketChannel继续追溯发现是某个长连接因为心跳超时策略没生效连接未被回收底层发送缓冲区一直积压消息。整条链路上各个对象的职责一目了然。3. 核心功能二深堆、保留集和泄漏判定3.1 浅堆与深堆的区别先算一笔账很多人被“Shallow Heap”和“Retained Heap”绕晕。其实概念不复杂。浅堆Shallow Heap对象本身占的内存不包括它引用的其他对象。比如一个HashMap对象浅堆只是它内部数组和几个字段的开销数组里的Entry对象不算。深堆Retained Heap对象被回收后连带释放出来的所有对象的总内存。换句话说深堆是“保留集Retained Set”的大小。保留集就是从该对象出发只能通过它到达的那些对象集合。为什么深堆更重要因为判断一个对象是内存消耗大户看的是“把它干掉能省下多少内存”而不是“它的壳有多大”。很多对象浅堆很小但持有的子对象链很长深堆大得惊人。我遇到过一个解析大JSON的案例一个JsonObject实例的浅堆只有几百字节但它的深堆达到60MB因为它引用了整棵解析树、原始字符串缓冲、嵌套的List等。只看浅堆会把它忽略按深堆排序它就是头号嫌疑。在MAT界面里每个对象旁边会同时显示浅堆和深堆两列数字。你可以配一个右键操作选择Calculate Retained Sizes让MAT完成深堆计算。对于超大堆深堆计算可能耗时较久这是正常现象我一般会同时打开Calculate Minimum Retained Sizes获得一个快速近似值先粗筛再精算。3.2 Path to GC Roots内存泄漏的“证据链”MAT里我最常使用的右键功能就是Path to GC Roots。它能列出从GC Root到选中对象的完整引用链是判断“这个对象为什么回收不掉”的黄金路径。GC Roots在JVM里主要包括线程栈中的局部变量、静态变量、JNI引用、被锁对象等。如果某个对象到所有GC Root都没有路径它早被回收了。所以当对象存活且内存高必然存在一条根引用链。MAT可以把这条链展开并标出每个引用类型。顺着链往下看你能发现问题通常出在某一个环节一个被static修饰的集合缓存了业务数据、一个ThreadLocal把请求对象挂在了一个池化线程上、一个回调注册后被遗忘而一直持有观察者。这里有个高阶技巧在结果界面里切换Merge shortest paths to GC roots把多个对象到根的最短路径合并展示。如果大量无用对象都汇聚到同一个类或同一个线程说明祸根集中如果路径发散那可能是整体缓存策略的问题而不是单个点Leak。操作时注意这个命令会增加分析耗时建议先选定可疑对象集合再执行而不是全堆分析。3.3 Retained Heap的计算思路与对照表MAT的Retained Heap计算实际上是一个“引用图可达性”问题。设对象A被回收后所有仅能被A直接或间接引用的对象都被回收这些对象的总浅堆和就是A的深堆。计算时MAT对每个对象维护一个引用邻接表然后对堆中每个对象做遍历按支配者关系累加。这个过程是算法层面的事但作为使用者知道它是什么含义就够用了。下面用一个小模型做参数对照方便大家记忆术语含义典型判断场景Shallow Heap对象自身字段占用的内存判断对象本身大小Retained Heap对象回收后连带释放的内存判断“干掉它能省多少内存”Retained Set只能经由该对象到达的其他对象集合分析容器、缓存持有大量子对象Dominator Tree支配关系的树形展示定位整个内存分组的“根”这里建议每次分析时把“Shallow Heap大但Retained Heap小”和“Shallow Heap小但Retained Heap大”两类对象分别看。前者通常是数组扩容过度或内部字段过大后者则意味着对象引用了大量子内容是真正的“内存地主”。4. 核心功能三OQL对象查询语言顺藤摸瓜的利器4.1 OQL能干什么MAT内置了一个类似SQL的查询语言叫OQLObject Query Language。它允许你通过类名、字段值、引用关系等条件精确筛选堆中的对象并支持对结果做计算、排序、分组。和界面上的点选操作相比OQL适合回答“出这种特征的对象有多少”“都分布在哪个包下”这类具备条件化的问题。以搜索热词里提到的“两个矩阵提出数据2相减再算绝对值”为例——那其实是MATLAB场景不在Eclipse MAT范围内。但用OQL你确实可以做到按字段取值、做运算比如查询某个业务对象里所有价格字段大于100的记录或者统计一个Map中超过某个阈值的Entry。完全可以把它当成一个面向堆内对象的小数据库。OQL的查询入口在工具栏“OQL”选项卡输入语句后点击执行。基础的select语法和SQL很想但对象字段访问用的是Java反射风格比如select c.fieldName from com.example.User c。常用函数包括toString()、toDisplayString()、asArray()等。我建议新手先学会几个固定模板再逐步扩展。4.2 几个高价值OQL实例列出某个类创建的所有对象并过滤字段select * from com.example.Order o where o.status 1按包分组统计对象数量和总深堆select dominantClassOf(OBJECTS) as clazz, count(OBJECTS) as cnt, sum(retainedSize(OBJECTS)) as total from java.lang.Object group by dominantClassOf(OBJECTS)筛选字符串内容重复的实例select s.toString() as str, count(*) as cnt from java.lang.String s group by str order by cnt desc找出所有数组长度超过1000的byte数组select b.length, b from byte[] b where b.length 1000第一个语句最常用因为它能快速回答“某种特征的对象有多少”。第二个语句适合全堆聚合帮你看清内存分布占比。第三个我在排查字符串重复时就用过一条指令把重复字符串的排序名单拉出来再逐个用Path to GC Roots追源头。第四个语句用于定位大缓冲区。需要注意在输入字段名时如果字段是私有类型可能需要用语法或开启对非公共字段的访问MAT默认有内省能力某些深层字段访问不到了可以换用select ... from 类的全限定名。OQL的性能也要提一句对超大堆执行全表扫描式查询可能很慢甚至卡死。建议先用Histogram或者正则缩小范围再对具体类执行OQL。还有一个经验用OBJECTS关键字把“对象本身”提取出来再配合retainedSize(OBJECTS)要么统计精确要么放弃不要在大堆上反复试错。5. 核心功能四线程分析与内存关联指标5.1 Thread Overview看线程和栈帧持有对象很多内存问题不是“对象太多”而是“线程持有的对象没释放”。MAT的Thread Overview功能可以列出堆转储时刻所有线程的状态、栈帧、以及每个线程关联对象的内存占用。这个视图特别适合排查线程池背景线程悬挂、ThreadLocal导致对象长时间存活、以及请求线程卡在某个IO上而堆积数据。进入方式打开堆转储后在“Windows”菜单中找到“Threads”或者直接在导航区选择“Thread Overview”。界面会展示每个线程的堆栈树展开栈帧可以看到帧内局部变量引用的对象同时显示它们的浅堆和深堆。一次典型的分析是发现某个线程的深堆最大展开后看到它的Poket引用了一个巨大的集合集合内容本应在请求结束清空但ThreadLocal没清理导致线程池中的下一个任务继续见到旧数据。这里要重点提醒Thread Overview里看到的是“堆转储瞬间”的状态不是实时的。如果一个线程当时正在处理大数据量请求它的栈上引用很正常不代表泄漏。因此我会结合上下文判断如果这个线程是常驻的池化线程且持有对象跨多个任务周期那才值得怀疑如果是瞬时业务线程它的大深堆可能只是“正在干活”等它结束后就可能释放。5.2 Top Consumers堆内各区域的内存消费排行MAT的顶部分析中还有一个容易忽略的入口是Top Consumers。它按包、类加载器、类三个维度分别统计浅堆和深堆的Top N。这个视图的价值在于“视角切换”有时候从类维度看不出问题但从包维度能发现某个框架内部包整体占了大部分堆。打开类似报表后MAT会给出一个“按包路径拆解内存”的饼图列表还能逐级下钻到具体类。举个例子之前分析一个网关服务Top Consumers显示某个公司内部RPC框架包名下的对象总深堆达到几百MB。起初以为是业务对象问题后来发现是框架的Response池化机制没有正确释放所有历史调用响应对象都被缓存在一个静态数组里。如果我只盯着业务包根本不会注意到框架内部的问题。这里强烈建议养成看Top Consumers的习惯把包名维度的排行作为直方图的补充。还有一个功能是Leak Suspects Report里的详情链接。它会把可疑根对象与“Thread Overview”“Histogram”“Dominator Tree”互相跳转这一套联动链路很高效。我通常的执行顺序是先看Leak Suspects报告再用Top Consumers看包级分布再用Histogram和Dominator Tree精确定位具体对象最后用Thread Overview确认是哪个线程在持有整个排查半小时内就能完成。6. 常见问题与排查技巧实录6.1 堆转储文件太大导入慢怎么办实际生产环境的堆可能超过8GB甚至20GBMAT直接打开会很吃力甚至直接卡死。有如下几个处理办法用jmap导出时加-dump:live减少其已死对象文件体积能小很多。调整MAT自身内存打开安装目录下的MemoryAnalyzer.ini把-Xmx设置到物理内存允许的较大值比如-Xmx8g。需要先保存为-Xmx格式不要写错位置。使用File - Open Heap Dump时选择只打开Dominator Tree或先不加载全部细节MAT支持延迟加载部分视图。如果文件实在大考虑先用jhat或IDE内置分析器做粗筛再用MAT针对关键子图深入。很多人忽略的一点是堆转储里的很多对象是“可压缩”的比如字符串表、重复类元数据。MAT在Open Heap Dump时可以选择Keep unreachable objects取消勾选可以丢弃不可达对象显著减小分析集我第一次用这个选项把9GB的堆压到2GB级别分析速度肉眼可见地提升。6.2 为什么MAT报告的可疑对象不是真正泄漏点Leak Suspects报告常见的一个问题是“误报”。它的判断基于保留集大小和引用链特征但大型系统的缓存、预热池、正常冗余都会被认为是可疑。我处理过不少线上问题报告指出某个静态Map很大实际那是一个设计如此的路由表只是数量级偏大。因此看到报告不要直接删代码。先问三个问题这个对象有没有随时间线性增长它有没有过期机制它是否被多线程共享且写入路径明确如果前两个答案都是“否”基本可以确认为泄漏。如果是“有缓存设计”的正常对象那问题可能不在“该不该有缓存”而在“缓存淘汰是否失效”。MAT能帮你把对象抓出来推理判断还得靠人对业务的把握。6.3 MAT和MATLAB、.mat文件怎么区分再强调一次命名混淆问题。搜索热词里的“mat数据”“eclipse mat下载”是Eclipse Memory Analyzer Tool输入文件是.hprof而“matlab .mat文件 两个矩阵提出数据2相减再算绝对值”完全是MATLAB的二进制.mat文件处理属于数值计算领域。两者除了缩写相似技术栈毫无关系。如果你在做Java维测应该下载的是Eclipse MAT的发行包从Eclipse官网或者国内镜像获取均可如果下载成了MATLAB你会发现根本打不开.hprof。区分要点就一条看你的目标文件是后缀.hprof还是.mat。看到Word里有“java memory analyzer”关键词的是前者准没错。这个提醒虽然简单但真能帮人少走弯路。我在实际排查中还有一个习惯每次分析完把最可疑对象的截图、引用链路径、OQL语句整理成一小段文档放回工单里。这样下次再看问题时资料是闭环的。排查内存问题最怕“分析一时爽复盘没资料”MAT的所有功能都是围绕“定位”设计的真正让它发挥价值还得靠使用者的耐心和条理。