
做Java服务端的朋友十有八九都遇到过内存溢出的情况系统跑着跑着CPU飙高、接口超时甚至直接OOM宕机重启后过一段时间又复现。这时候MAT——Eclipse Memory Analyzer就是我最先掏出来的工具。别把它想得多玄它就是一个专门用来分析JVM堆转储文件heap dump的可视化工具帮你把堆里到底存了什么、谁占了内存、泄漏点在哪一层层扒清楚。这篇文章不打算给你讲一堆晦涩的理论而是把我自己这几年用MAT排查问题的完整套路、踩过的坑、还有那些通常没人写在文档里的细节一次性讲明白。适合刚入门的新手也适合已经用过但不一定用得溜的同学。1. 工具是什么MAT的定位与适用场景1.1 什么时候该掏出MAT很多人把MAT当成一个“OOM之后才用”的工具这其实有点把它的能力想窄了。我用下来的经验是只要跟JVM堆内存相关的问题基本都能靠它理出头绪服务频繁Full GC接口响应越来越慢你怀疑有对象在堆里堆积但不知道是什么对象线上直接OutOfMemoryError业务方催得急需要快速定位是哪块业务代码把堆撑爆了压测之后内存一直降不下来怀疑有内存泄漏但通过代码审查一时半会找不到嫌疑对象怀疑某个全局缓存、静态集合、ThreadLocal把对象长期引用住需要看GC Roots引用链来确认想让某个高消耗的存活对象“现出原形”比如大字符串、大数组、连接池对象数量异常膨胀。说白了MAT干的事情就是把JVM某一时刻的堆快照heap dump读进来然后通过对象数量、占用的浅堆/深堆大小、引用关系等维度帮你回答三个问题——堆里有什么谁占得最多这对象是怎么被到达引用的。这三个问题一旦有答案后面改代码就有了目标。1.2 先建立直觉一个典型的OOM排查现场我举个这几年最常遇到的例子。某个定时任务系统每天凌晨跑批跑一段时间后半夜必定OOM运维重启后第二天又复现。这种问题最头疼的地方在于不是一启动就会挂而是运行一段时间才爆发典型的“缓慢泄漏”特征。我们的排查流程是这样的先给服务加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/dump.hprof等第二天OOM发生之后拿到dump文件然后用MAT打开直奔Leak Suspects Report。报告里往往直接画出一条主导线xxxService里的一个静态HashMap不断往里put数据却没清掉key还是一个每天新增的日期字符串——这样日积月累堆就被撑爆了。这个场景引用了一个关键经验内存泄漏排查早就不是看代码就能拍板的事情借用工具对堆对象做定量分析定位速度会快得多。下面就从安装准备开始把每一步操作讲透。2. 准备与连接下载安装、拿到Heap Dump2.1 下载、安装与自身内存配置MAT是基于Eclipse RCP构建的独立工具官方叫“Memory Analyzer”现在一般从Eclipse官网下载独立发行版解压就能用不需要再额外装Eclipse全家桶。注意两个细节版本选择如果你的JDK是17甚至21建议下载最新版的MAT2023年以后基本上都是基于Eclipse 2023-06 构建的版本老版本在解析某些新JDK产生的堆转储时可能出现格式不兼容或者直接打不开的情况。运行环境MAT本身是Java程序它需要自己的JVM来跑。虽然默认配置就能启动但在解析几个GB的大堆dump时默认的内存参数大概率不够用。这里请务必打开安装目录下的MemoryAnalyzer.ini文件找到类似这两行的位置-Xms1024m -Xmx1024m建议根据你机器的物理内存把-Xmx改成至少4096m甚至更高。我的经验值是解析的dump文件大小最好不要超过MAT自身堆内存的50%~60%。比如你要分析一个4GB的heap dump那MAT自身-Xmx最好给到8GB否则后面点击对象、计算保留堆时经常卡死。改完重启MAT。提示不要只改-Xmx不关心-Xms两者保持一致可以避免运行过程中频繁扩容导致的额外停顿。2.2 三种拿Heap Dump的方式MAT分析的第一步是手里得有一份heap dump文件。拿dump的方式很灵活我常用的有三类方式一通过jmap手动导出这是最传统、最直接的方式jmap -dump:live,formatb,file/data/logs/dump.hprof pid-dump:live表示先触发一次Full GC只保留存活对象再导出formatb指定hprof二进制格式。注意加了live导出的dump比较小因为死对象被过滤掉了但如果你的目的是排查“为什么对象没被回收”建议不要加live否则你根本看不到那些本该回收却没回收的对象。方式二通过jcmd导出jcmd是JDK 7之后官方推荐的诊断命令跟jmap的用法还有一点区别。先通过jcmd pid GC.heap_dump /path/dump.hprof导出注意这里不需要-dump:前缀格式更加简洁。如果你面对的是容器环境经常能在镜像里找到jcmdjcmd 12345 GC.heap_dump /data/logs/dump.hprof方式三通过启动参数自动生成这是我最推荐上生产环境的方案。在JVM启动参数里加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/dump-pid.hprof这样一旦发生OOMJVM会自动把当时的堆快照写到指定文件。好处是抓的是OOM现场的第一手数据不需要人工介入而且Dump发生在即将崩溃前堆状态最接近问题爆发的真实情况。在线下环境如果你想在没有OOM的时候“预防性”地抓快照也可以考虑jhsdb jheap dump --pid pid它是JDK 9之后提供的新工具之一。不过日常排查我还是优先jmap和jcmd稳字当头。2.3 生产环境导出dump的时机与注意事项在生产环境手动导dump是一个非常敏感的操作。因为导出过程会触发一次STWStop-The-World停顿尤其在堆很大、对象很多的时候停顿时间可能达到几十秒甚至分钟级别。所以你要提前评估优先选择业务低峰期或者在多实例服务中先摘除某一台实例的流量再导出不要用jmap -dump对一台几十GB堆的在线服务反复操作否则很容易把可用性打没如果只是定位普通的内存占用问题先用jmap -dump:live缩小体积如果是怀疑泄漏最好关闭live过滤保留完整堆快照导出完成后立刻用gzip压缩文件hprof文件压缩率极高动不动就能压到原来的十分之一方便你拉回本地慢慢分析。3. 首次打开Dump关键视图与第一层判断3.1 打开dump后如何选择分析维度用MAT打开一个hprof文件后第一屏通常是一个向导里面有Leak Suspects Report泄漏嫌疑报告、Component Report、Overview概览等选项。很多新手会直接点Leak Suspects这没问题但我建议第一次打开一个大堆dump时先点Overview看整体面貌。Overview页面会显示总堆大小确认这份dump是不是真的跟问题规模相符类加载器数量如果数量异常多很可能有类加载器泄漏对象总数、Class总数这些基础指标能快速帮你建立直觉还有几个快捷键入口最常用的是Histogram和Dominator Tree。这里还藏着一个实用功能Overview右上角的“Overview”视图右侧有个“Actions”区域可以一键打开Leak Suspects Report、Histogram、Dominator Tree、Top Consumers。多花十秒钟看概览再决定下一步点哪个属于“磨刀不误砍柴工”。3.2 Histogram用“直方图”看对象分布Histogram是整个MAT里我用得最多的视图。它本质上是一个表格按Class类型统计对象数量并给出两个关键列Shallow Heap浅堆对象本身占用的内存不包括它引用的其他对象Retained Heap保留堆如果把这个对象当作根来回收连带能释放多少内存通俗说就是“这个对象真正撑住的内存吞吐量”。注意排序列一定要选Retained Heap因为浅堆大小很多时候很有欺骗性。比如一个HashMap$Node的Shallow Heap可能只有几十字节但如果它挂着一条长长的链表Retained Heap可能是几MB。我曾经排查过一个“看似很小的字符串列表把堆撑爆”的案例单看Shallow Heap完全找不到问题按Retained Heap从大到小排序后问题对象立刻浮到顶部。还有一个容易被忽略的小技巧右键任意Class选择“Merge Shortest Paths to GC Roots”选择“exclude all weak/soft references”。这一步能把被弱引用、软引用、虚引用“掩盖”的对象路径过滤掉直接看强引用链这样能更快区分“真泄漏”还是“假活”。3.3 Dominator Tree顺藤摸瓜找支配关系Dominator Tree支配树是按“支配关系”组织的对象视图。什么是支配关系简单说从GC Roots出发如果所有能到达对象B的路径都必须经过对象A那么A支配B。举个例子一个线程对象如果被某个连接池对象强引用而连接池对象又被静态变量引用那连接池对象就是支配者。我把Dominator Tree当“排序后的对象依赖关系树”来用打开之后默认按Retained Heap从大到小排列你一眼就能看到堆里最大的那几棵“顶树”是什么。如果想看某个大对象背后到底是谁在引用它右键 -Path to GC Roots选择show all paths就能看到所有引用链条。这里有个理念性问题看到一个大对象不等于找到了泄漏。比如一个很大的byte[]数组它可能是一个大文件读取缓存也可能是业务数据本身。真正的判断节点在于“这些对象是否一直存在、是否还在增长、是否被业务强引用着不被释放”。3.4 Thread Overview线程角度排查在怀疑“某个线程栈持有大对象”或者“线程数量异常”的场景里Thread Overview非常有用。它会把dump时所有线程列出来每个线程都关联一个Retained Heap并且能看到线程当前的栈帧信息。我印象最深的一次某个服务线程数已经冲到两千多看起来像是线程泄漏。用Thread Overview一看大量线程阻塞在java.net.SocketInputStream.socketRead0Retained Heap各有几百KB。一查代码原来是一个HTTP调用没有设置连接超时和读超时上游接口卡住导致线程池被占满同时每个线程都持有自己的请求上下文对象堆内存被这一批批“卡住的线程”吃光。这种问题如果不从线程维度切入单看Histogram很难想到是线程本身造成的。Thread Overview还有一种用法当怀疑“某个任务执行后没释放局部变量”你可以对比几个工作线程看哪些线程Retained Heap异常偏高再逐个点上Thread Dump看栈帧配合代码就能直观发现哪个方法“留了尾巴”。4. 深入定位Leak Suspects、OQL与引用链确认4.1 Leak Suspects报告的正确打开方式当你想快速得到“疑似泄漏点”的汇总Leak Suspects Report确实是第一选择。它会把dump中疑似泄漏的对象聚合成一个个“suspect”每个suspect给出占用的保留堆大小嫌疑对象的类型引用链的简述往往直接指向某个类加载器、某个集合类、某个业务对象。打开方式很简单工具栏点“Leak Suspects”或者从Overview的Actions区进入。报告生成后通常第一个suspect的Retained Heap就占了整个堆的70%以上优先看它。但这里我必须泼一盆冷水自动报告出来的“嫌疑对象”并不等于就是“泄漏的发生者”。它很多时候只是告诉你“有一个巨大的对象结构体”你还要继续追查入口。我见过太多人看到Leak Suspects写了java.util.HashMap就直接断定是“HashMap泄漏”结果HashMap其实是某个业务缓存真正的原因是无界使用不是Map本身的问题。所以正确姿势是用Leak Suspects做初步方向然后跟到对象本身右键Path to GC Roots回到代码逻辑里去确认入口和生命周期。4.2 从对象到GC Roots引用链把代码揪出来真正的“定案”阶段我几乎每次都走同一条链路在Histogram里锁定某个Retained Heap偏大的Class右键打开该Class的实例列表List objects - with outgoing references在实例列表里再选中一个典型对象右键 - Path to GC Roots - show all paths去掉软引用、弱引用、虚引用以及JNI引用等干扰项最终观察是否有一条“非常不合理”的强引用链比如被某个静态集合长期持有。这一步的核心是想清楚一个问题对象为什么没有被回收只有当GC Roots路径上的某个节点是业务代码里的静态字段、常量、缓存容器、线程对象时你才能确定泄漏源在哪个类里。一个非常典型的例子你在Path to GC Roots里发现一个业务对象被ThreadLocal持有而ThreadLocal又是被某个线程对象持有的。再往上看这是一个不会被销毁的长生命周期线程那么“线程池内ThreadLocal变量未remove”就是根因。这种链条不看引用路径光看对象本身是完全得不出结论的。4.3 OQL比界面更灵巧的堆内查询MAT自带一个类似SQL的查询语法叫OQL入口在工具栏的“OQL”按钮。它跟SQL很像但你能直接对堆里的对象做条件过滤特别适合在几百万个对象里找出特征明显的目标。我常用的几条OQL先给你贴上-- 找出所有长度超过100万的字符串 SELECT s FROM java.lang.String s WHERE s.value.length 1000000-- 按Character数组大小降序查看最大数据块 SELECT s.value.length AS len, s FROM java.lang.String s ORDER BY len DESC LIMIT 20-- 找出所有HashMap实例并显示Size属性如果有的话 SELECT h FROM java.util.HashMap h-- 找出所有没有eleData的byte[] 长度超大的数组 SELECT a FROM byte[] a WHERE a.length 1048576还有一些进阶写法比如查对象引用SELECT OBJECTS java.lang.reflect.Field.f FROM java.lang.reflect.Field fOQL的语法文档网上很多但我的实际建议是优先掌握三件套——按字段长度/数值过滤、按类型查询、排序输出TOP N。如果连这都不会用绝大多数场景你切回Histogram右键操作也来得及OQL的价值在于批量筛选和自动化脚本化比如一次查询吐出几十个嫌疑对象列表再去逐一核对。4.4 结合代码与GC日志验证结论很多人到这里就收工了但这恰恰是实战中最容易翻车的一步。MAT给出的所有结论都只是堆快照层面的统计学证据它证明了“此刻堆里有这样一个对象结构”不直接证明“这就是导致系统的内存问题的根因”。因此在改代码之前我强烈建议再做两件事打开同一时段的GC日志。如果gc.log里显示老年代持续上涨而年轻代回收后对象年龄一直变化不大那说明对象大概率是“存活对象”而不是“频繁创建未回收”反查业务代码中的创建和释放逻辑。比如你用MAT看到一个LinkedList中有上千个订单对象那就需要确认它的生命周期是不是应该随请求结束而销毁如果答案是否定的那这就是根因。我习惯把MAT的结论当成“嫌疑人名单”把代码逻辑和GC日志当成“审讯证据”两相结合才敢在发布会上拍板说“就是这里泄漏了”。5. 实战避坑建议与排查心得5.1 常见操作误区和新手易踩的坑这几条都是我在内部培训或者带新人时反复强调的属于“文档里不一定写但实际绊倒过无数人”的细节别一上来就Leak Suspects也别完全不信它。先看Overview和Histogram心里对堆构成有个谱再用自动报告去验证你的猜测这样被报告误导的概率低很多。Retained Heap不等于“这个对象需要被释放的内存”。它只是一个计算值不同视角的“Retained Set”定义略有差异不要拿着两个对象的Retained Heap做绝对精确的加减。导出dump时如果用了live那Full GC已经把弱引用清掉了你看不到已经被回收前的临时对象。排查“瞬时大量对象”问题时需要不带live导出一份完整堆。警惕MAT解析大文件时的内存爆掉。前面说过MemoryAnalyzer.ini里的-Xmx一定要按需调整。如果机器内存有限可以用-Xmx4096m配合-Xms1024m然后尽量分析压缩后的dump。5.2 一个大dump解析卡顿或解析失败怎么办我碰到过几次“文件有8GBMAT打开后直接白屏”的情况处理顺序很实用先确认物理机可用内存确实够大再检查MemoryAnalyzer.ini的-Xmx如果仍然卡尝试用jhat或者jhsdb jmap histo先跑一个类级别的统计大致预览哪些类占比最高也可以考虑用Eclipse MAT的“Reduce Dump”功能新建一个只保留特定包名或特定Class的dump子文件然后对新文件再做分析实在不行用-XX:HeapDumpOnOutOfMemoryError重新生成一个较小的dump配合live参数控制体积。这里还要说一个土办法用gzip压缩hprof文件后MAT其实是支持直接打开gz压缩格式的。你没必要把几GB的原始文件拉到本地直接在服务器上压缩再把.gz下载下来MAT解析时会自动解压。5.3 对比法用两个dump定位“增长点”对于“服务运行一段时间才爆”的渐进式问题单个dump的参考价值有限更好的做法是在不同时间点各导出一份dump然后用MAT的Compare功能做对比。具体用法在线下或压测环境中t1时刻正常业务运行中导一份dump让服务继续运行等内存涨到警戒值附近t2时刻再导一份dump打开MAT把两份dump都加载到同一个Session里右键任意一个类 - Compare - 选择另一份dump看两个dump中各类的Object Count和Retained Heap差异增长最快的那个类就是要追查的方向。这个方法的威力在于能抹掉“本身就应该占用的大对象”的干扰只放大“从t1到t2这段时间新增的占用”。有一回我们用一个很不起眼的java.util.LinkedHashMap属性对比出来它增长了两万多条记录最后定位到是因为接收了外部消息后没有做幂等去重——这种问题靠静态看代码几乎找不到。5.4 我常用的一套餐固定步骤最后归纳一下我现在排查线上OOM的标准动作供你参考先加-XX:HeapDumpOnOutOfMemoryError部署等下次OOM自动出dump用jcmd或jmap在业务低峰期采集第二份“内存高峰前”的dump用MAT打开OOM dump先看Overview确认总堆规模和对象总数进Histogram按Retained Heap排序圈出前20个嫌疑类对嫌疑类实例做Path to GC Roots剔除软/弱引用找到强引用入口用OQL进一步筛选特征对象比如超大数组、超多key的Map结合GC日志和业务代码确认根因修复后按同一场景压测复测顺手用两份dump对比验证内存曲线是否回归正常。这套流程看着长熟练之后其实半小时内能跑完一轮。尤其是手头有自动出Dump的机制时整个排查节奏会非常顺。6. 最后再分享一个小技巧用MAT这几年我最深的体会是这个工具真正的门槛不在工具本身而在你怎么定义问题。你只有清楚“我要找的是哪个时间点的哪个对象结构”才能把Histogram、Dominator Tree、OQL这些功能用到刀刃上。所以我强烈建议你在平时压测时就经常导dump、开MAT看看堆结构先混个眼熟等线上真出了OOM你就不会慌因为你对整个堆的平时状态已经有了直觉。另外一个小习惯值得保留每次分析完一个dump把关键截图和OQL语句记到团队的wiki或者笔记里。下一次遇到相似问题翻旧笔记往往比重新分析一遍更快。这就是我在实践中最真实的感觉——MAT只是抓手真正值钱的是你对自己系统堆内存“正常长什么样”的那份了解。