简介:MemoryAnalyzer 1.6.1 的 Windows 版压缩包(2016 年 11 月 25 日构建)定位清晰,是面向 Java 开发者和运维人员的内存分析工具,用于读取堆转储文件,定位内存泄漏与对象异常占用。它由 Eclipse 基金会维护,借助支配树、对象直方图和 Leak Suspects 报告,可快速排查对象引用链,识别未关闭的数据库连接、静态集合中的残留对象等常见泄漏源;支持对比多个堆转储,观察对象增长趋势,为优化代码和 JVM 参数提供依据,进而判断哪些对象无法被垃圾回收。压缩包共 463 个文件、约 57.99MB,以 jar 核心组件、xml/properties 配置、html/css 帮助文档以及 png/gif 界面素材为主,同时包含 exe/dll/bat 运行入口,解压后即可在 32 位或 64 位 Windows 环境中直接使用。包内说明文件对初次使用者很有帮助,结合启动脚本可减少环境配置成本。目前已有 301 人学习下载,适合正在排查 JVM 内存问题、开展性能调优的中高级开发者参考。
1. 一个 2016 年的 MemoryAnalyzer 1.6.1:为什么至今还是排查 Java OOM 的第一工具
线上 Java 服务半夜 OOM,重启完第二天照旧,这种问题不靠猜,得靠堆转储快照说话。MemoryAnalyzer(业内直接叫 MAT)就是专门读 .hprof 快照、定位谁把内存拖垮的分析器,标题里这个 1.6.1 是 1.6.x 系列里流传最广、最常被归档复用的稳定版。
它不解决“为什么报 OOM”这类表面问题,而是三件硬事:找到嫌疑泄漏对象、还原 GC Root 到对象的完整引用链、用 OQL 把几千万对象当表来查。后端排查、中间件调优、缓存复盘都绕不开。
适合被 OOM 折腾过的 Java 服务端开发与性能定位工程师。老手用它翻旧案,新手用它学引用链,这份 Windows x64 版同时满足两侧诉求。
2. 部署到首份堆转储:从 zip 解压到能打开 6G 大堆
拿到 MemoryAnalyzer-1.6.1.20161125-win32.win32.x86_64.zip 之后,别急着双击 exe。先理顺目录、JRE、内存参数三件事,否则后面每打开一个 hprof 都会跟环境问题纠缠。以下按我实际操作的顺序来,照着走基本一次过。
2.1 解压与运行环境核对:纯英文路径是底线
这个 zip 解压后是一个完整的 RCP 目录,MemoryAnalyzer.exe 和 plugins、configuration 同级摆放。文件名里的 win32.win32.x86_64 是平台打包标识:win32 指 Windows 图形栈,x86_64 指 64 位架构,所以它不是给 32 位系统用的。我第一次用时把它解压到带中文的路径下,双击 exe 没反应,事件日志里也看不出所以然,换成纯英文路径后一切正常。目录名同样别带空格和特殊符号,这是老 RCP 应用的通用毛病,不是玄学。
MAT 不自带 JRE,启动靠的是系统 PATH 里的 java。核对命令很简单:
# 确认 PATH 上的 java 是 64 位 java -version 2>&1看到 64-Bit Server VM 字样就对了。机器上装了多个 JDK 时,先临时把 JAVA_HOME 指向 64 位版本,再启动 MemoryAnalyzer.exe,否则会闪退或者报找不到虚拟机。这一步翻车最常见,但解决也就一分钟。
2.2 MemoryAnalyzer.ini:Xmx 与 GC 参数怎么配才稳
1.6.1 默认的-Xmx1024m只适合玩小 dump。解析 hprof 时 MAT 要先建立快照索引、再算保留大小,吃内存比想象中狠很多。我的一般规则:Xmx 取 hprof 文件体积的 2~3 倍,但上限不超过物理内存一半。比如一个 3G 的堆转储,-Xmx给 6G 起步;机器只有 8G 物理内存就不要再往上顶,MAT 解析时本身还有索引和 UI 开销。
打开安装目录下的 MemoryAnalyzer.ini,找到-vmargs段,改成这样:
-vmargs -Xmx4096m -XX:+UseConcMarkSweepGC第一行-vmargs是固定标记,告诉启动器后面都是 JVM 参数。第二行把堆上限抬到 4G,这是打开 4G 级 hprof 的起步线。第三行-XX:+UseConcMarkSweepGC是 1.6.1 时代的推荐 GC,解析大文件时停顿少。如果你分析机上装的是很新版本的 JDK,这个 CMS 参数可能会不被识别,那就换一个与 MAT 同年代的 JRE 来跑这套工具,别在启动参数上硬顶。
| 参数 | 作用 | 建议取值 |
|---|---|---|
| -Xmx | MAT 自身堆上限 | hprof 体积的 2~3 倍,不超过物理内存一半 |
| -XX:+UseConcMarkSweepGC | 降低解析时 GC 停顿 | 老 JRE 保留;新 JRE 识别不了就删掉 |
改完保存重启。第一次启动会问 workspace 目录,默认即可;但每次解析过的 dump 都会在 workspace 里留下 .metadata 索引文件,如果连续翻车,可以在 ini 里加一行-data指定全新目录,相当于给工具吃了后悔药。
2.3 三招拿到堆转储:OOM 自动落盘、jmap、jcmd
分析器备好了,得先有数据。三招按推荐度排:
# 方式一:JVM 启动参数,OOM 时自动落盘(生产环境强烈建议开) # 在应用启动参数里加: # -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dumps/ # 方式二:jmap 手动抓,老版本 JDK 也用这招 jps -l jmap -dump:format=b,file=/data/dumps/app.hprof 12345 # 方式三:jcmd 抓取,新版 JDK 更推荐 jcmd 12345 GC.heap_dump /data/dumps/app.hprofjps -l先拿进程号,12345 是目标 pid。jmap 不带live参数导全量堆;带了live会先触发一次 Full GC,很多还没来得及回收的临时对象就从 dump 里消失,排查泄漏时容易漏线索,所以我一般建议不带。jcmd 是后续 JDK 里官方推荐的替代,参数更规整。抓完把 hprof 拷回本地再解析,别在服务器上跑 MAT,一次 8G 堆的解析瞬间能把生产内存吃满。
注意:hprof 文件别放在网络映射盘上直接解析,I/O 延迟会让“打开文件”这一步慢到像死机。先拷到本地固态盘再开。
3. Leak Suspects、Histogram、Dominator Tree:三份报告串成泄漏证据链
报告不是用来“看看结论”的,而是三份层层递进的证据。我的习惯顺序是:先 Leak Suspects 拿嫌疑名单,再 Histogram 验证类维度占比,最后 Dominator Tree 把引用链钉死。任何一个环节单独用都可能误判,特别是只扫一眼结论就下判断。
3.1 打开 hprof 时两个关键选项:Keep Unreachable Objects 要不要勾
File → Open Heap Dump 选中文件后,MAT 会弹一个解析选项框,两件事要做决策:要不要勾 “Keep unreachable objects in analyzing result”,以及要不要自动生成报告。
不可达对象指的是 OOM 爆发前已经失去引用、但还没来得及被 GC 回收的对象。它们占着的堆空间,恰恰可能是泄漏的主体——比如一个方法栈帧被异常中断后遗留的大 byte[]。勾上它,分析结果更全,代价是解析时间和内存占用翻倍。我的做法是:默认不勾跑第一遍,如果 Leak Suspects 给出的结论站不住脚,再勾上重新解析一次。
报告选项里选 Leak Suspects,点 Finish 后 MAT 边解析边显示进度条,大 dump 在这时要等几分钟,期间别碰界面,也别开其他大程序抢内存。
3.2 Leak Suspects:先看嫌疑结论,再沿引用链找实锤
报告打开后,核心是一段结论文字加两张表。结论句的模式是 “One instance of X loaded by Y occupies Z bytes”,X 是嫌疑类,Y 是类加载器,Z 是保留堆体积。某次排一个订单服务的案例,结论指向 ConcurrentHashMap 里的一个内部数组,占了 1.4G,结合业务代码立刻定位到是缓存 key 设计失误。
“Shortest Paths To the Accumulation Point” 表展示从 GC Root 到嫌疑对象的每一环引用。每一行是链条上的一步,最底行就是嫌疑人。右键任意一行,用 “List objects → with outgoing references” 能展开该对象的出向引用,一层层验证是不是业务代码自己持有的,而不是框架内部噪音。
旁边的 “Accumulated Objects by Class in Dominator Tree” 则把嫌疑人间接持有的所有对象按类汇总,看它兜里装的都是什么货。这两张表一横一纵,基本能锁定泄漏现场,剩下的工作就是回代码里找对应创建点。
3.3 Histogram:先算 Retained Heap,再按保留堆排序
Histogram 按类汇总实例数、浅堆、保留堆三列。默认保留堆不计算,得先点工具栏的 “Calculate Retained Sizes” 按钮,等右下角进度条走完。这一步是全量计算,大 dump 下跑十几分钟都正常,前面把 Xmx 留够就是为了这一下。
浅堆(Shallow Heap)是对象自身字段占的字节,不含它引用的东西;保留堆(Retained Heap)是把该对象回收后连带释放的所有字节。两者差距巨大的类最有嫌疑。比如一个 1MB 的 byte[] 浅堆就是 1MB 出头,但它被 10 万个包装对象引用,类层面算下来的保留堆才是真实账本。只按浅堆排序,会把大头全部漏掉。
按保留堆降序排完,右键前三行,选 “Merge Shortest Paths to GC Roots”,可以看这些大类的共同根节点。如果多个大类的根都指向同一个业务容器,那这个容器就是泄漏核心,别犹豫,直接截图进故障单。
3.4 Dominator Tree 与 Path to GC Roots:从对象到持有链的最后一跳
Dominator Tree 是对象维度的树,父节点是子节点被回收的必经之路,每个节点显示保留堆,展开能看到它挡着哪些子对象。它的价值在粒度:Histogram 是类视角,它是具体实例视角。
定位到嫌疑节点后,右键 → Path to GC Roots → 选 exclude weak references。这一步很重要:弱引用、软引用本来就不阻止 GC,不排除的话路径里全是 WeakHashMap 的 ReferenceQueue 噪音,链条又长又绕看不出重点。排掉之后剩下的强引用链,才是“为什么 GC 不掉”的答案。
辅助视图是 Thread Overview,看每个线程栈帧持有的局部对象。如果泄漏对象被某条线程的局部变量攥住,这里比堆快照更容易看出是谁在跑。我之前排查过一个问题,结论就是某个异步任务的 task 对象被线程栈底部的一个局部集合一直引用,Thread Overview 一分钟就定位到了。
4. OQL 实战:把千万对象当数据库表来查
GUI 适合粗筛,真正做复盘还是得写查询。MAT 内置的 OQL 引擎能直接对堆内对象跑类 SQL 语句,有人笑称这是给 JVM 堆装的 SQL,用顺手之后效率翻倍。我把常用的几条存成模板,来新 dump 只改类名就跑。
4.1 OQL 骨架与 @ 伪字段
基本形态和 SQL 一致:SELECT、FROM、WHERE、ORDER BY、LIMIT 都有,差别在类型系统。FROM 后可以接具体类,也支持 INSTANCEOF 关键字做类型匹配。对象的内置属性用 @ 前缀访问。
SELECT toString(s) AS value, s.@retainedHeapSize AS retained FROM INSTANCEOF java.lang.String s WHERE s.value.length > 100 ORDER BY s.@retainedHeapSize DESC LIMIT 20这条查的是“内容超过 100 字符、保留堆最大的 20 个 String”。value 是 String 内部的 char[] 字段,length 取数组长度;@retainedHeapSize是内置伪字段,不用 join 就能拿。AS 别名、WHERE、ORDER BY、LIMIT 与写 SQL 手感基本一致。唯一要记住的是用toString(s)把对象转成可读文本,不然结果里是一串对象地址。
常用的伪字段就这几个:
| 伪字段 | 含义 |
|---|---|
| @retainedHeapSize | 对象保留堆大小 |
| @shallowHeapSize | 对象浅堆大小 |
| @objectAddress | 对象在堆中的地址 |
| @classLoaderId | 所属类加载器 ID |
| @GCRootInfo | GC Root 相关信息 |
4.2 三条高频查询:大数组、类总量、类加载器泄漏
第一条,找全堆最大的几个对象:
SELECT c, c.@retainedHeapSize AS retained FROM INSTANCEOF char[] c WHERE c.@retainedHeapSize > 10485760 ORDER BY c.@retainedHeapSize DESC LIMIT 2010485760 字节就是 10MB。跑完能看到是否有单块大数组占内存,典型来源是日志报文、图片解码、序列化中间缓冲。如果结果是一大批 10MB 级别的 char[],问题多半出在批量读文件或报文缓存没释放的场景。
第二条,统计某个类整体占比,用聚合函数:
SELECT sum(o.@retainedHeapSize) AS total, count(*) AS cnt FROM INSTANCEOF java.util.HashMap$Entry o想知道某个内部类整体占了多少、有多少实例,用 sum 加 count 最直接。注意内部类类名要带$分隔,这是新手最常见的报错来源。
第三条,查可疑类加载器:
SELECT cl, cl.@retainedHeapSize AS retained FROM INSTANCEOF java.lang.ClassLoader cl ORDER BY cl.@retainedHeapSize DESC LIMIT 10如果第一名 ClassLoader 占掉大半保留堆,且它加载的类路径全是动态生成的名字,基本可以断定是类加载器泄漏。再用@classLoaderId反查是哪些对象把它牵住,证据链就完整了。
4.3 OQL 与 SQL 的差异和三个报错
1.6.1 的 OQL 不是完整 SQL,三个差异要记住。一是对象内部字段访问要用.导航,伪字段才用 @;二是没有隐式 join,引用关系靠手写路径;三是聚合结果集是单行汇总,不支持再复杂排序。超时的话去 Preferences → Memory Analyzer → Queries 里调大超时时间,默认值对超大类查询经常不够。结果导出直接用右键 Export 成 CSV,放进故障报告当附件很好用。
5. 避坑清单:MAT 1.6.1 上翻过车的四个真实场景
这几条不是理论,是我和同事在真实故障排查里踩出来的,按“现象 → 原因 → 解决”写,方便你对着检查。
5.1 打开大 hprof 闪退或报 Could not reserve enough space
现象:点开 4G 以上的 hprof,进度条没走两步,exe 直接消失,控制台报 “Could not reserve enough space for object heap”。
原因:Xmx 设太大或太小都见过。太大时系统拿不出连续虚拟内存;太小时解析中途 OOM 把进程带崩。另外 workspace 里残留的上次解析索引也会在启动时被加载,加重内存压力。
解决:Xmx 调回 hprof 体积的 2~3 倍且不超过物理内存一半;在 ini 里加-data指定全新 workspace,清掉旧 .metadata;确认运行的是 x86_64 版 exe,而不是误用了 32 位启动器。
5.2 按 Shallow Heap 排序,结论完全跑偏
现象:Histogram 按 Shallow Heap 降序,一个 String[] 排第一,你认定它是泄漏源,改完业务代码,内存纹丝不动。
原因:浅堆只算对象自身字段,String[100000] 的浅堆也就几十字节,它引用的那堆 String 才是大头。泄漏关心的是“回收后能释放多少”,也就是保留堆。
解决:先点 Calculate Retained Sizes,计算完再按 Retained Heap 排序。如果浅堆和保留堆差距大的类占比高,说明对象间引用关系复杂,去 Dominator Tree 看链条,别在 Histogram 里下结论。
5.3 1.6.1 打不开新版 JDK 抓的 hprof
现象:选文件后直接弹 Unsupported Format,或者解析到一半说 Unknown tag,进度条卡死在某个百分比。
原因:hprof 格式在 JDK 演进中加了新 tag,1.6.1 的解析器不认识。这是老版本工具与新版本运行时的代差,不是文件损坏。
解决:先确认抓 dump 时目标 JVM 的版本,1.6.1 用老版本 JVM 的产物最稳。若生产环境已是新版 JDK,去找与 JDK 版本匹配的更新版 MAT 才是唯一出路,别在 1.6.1 上硬磨。如果必须留在 1.6.1,就让目标服务临时用老版本 JVM 启动并再抓一次 dump,对比两次结果也能定位问题。
5.4 引用链全是 WeakReference 噪音
现象:Path to GC Roots 结果几十行,从 ReferenceQueue 到 WeakHashMap 绕一大圈,什么也看不出来,结论都是假线索。
原因:没过滤弱引用。弱引用本来就不阻止 GC,真正的“根”应当是一条强引用链,而弱引用路径会把它淹没。
解决:右键目标对象 → Path to GC Roots → 选 exclude weak references,有些版本叫 exclude all phantom/weak/soft references。滤完通常只剩一条两三跳的强引用链,那才是要看的持有关系。看 Thread Overview 时同理,优先盯栈帧里非 static 的局部引用。
6. 进阶:无界面批处理与双 dump 对比,把 MAT 用成自动化工具
最后给两个我每季度至少用十次的技巧,把 1.6.1 从纯图形工具升级成流程化工具。
6.1 无界面模式跑报告
常见做法是在解压目录下用命令行参数直接触发解析,不进 GUI:
MemoryAnalyzer.exe -consolelog \ -application org.eclipse.mat.api.snapshot \ -vmargs -Xmx4096m \ /data/dumps/app.hprof加-consolelog走控制台日志,-application org.eclipse.mat.api.snapshot指定无界面解析应用,MAT 会解析 hprof 并用默认报告生成器输出结果。适合放在服务器上排程运行,早上直接看 report 目录,不用守着进度条。
6.2 两个时间点 dump 对比
抓一份服务启动后内存稳定的 dump 当基线,压测或故障发生后抓第二份。第二份打开后,在 Histogram 工具栏点 Compare,选基线文件,MAT 会按类列出增量:红色是新增占用,绿色是释放。逐个看增量大的类,再去 Dominator Tree 确认持有链,一张增量表就把“到底谁在持续长大”钉死了,比单看一份 dump 的绝对值更有说服力。
从一次凌晨四点的 OOM 复盘之后,我养成了死规矩:任何线上内存问题,先核 Xmx 和 workspace,再跑 Leak Suspects,补一份 OQL 大对象清单,最后双 dump 对比确认增量。宁可多花十分钟抓 dump,也不靠猜。希望帮到你。
本文还有配套的精品资源,点击获取