☰
MAT 1.6.1 Windows版:Java堆Dump与OOM泄漏排查
2026/10/27 19:11:38 网站建设 项目流程

简介:MemoryAnalyzer 1.6.1(2016年11月25日版)Windows 64 位压缩包,面向需要排查 Java 应用内存泄漏、优化堆内存占用的开发、测试与运维人员。工具围绕堆转储分析展开,提供支配树、直方图、泄漏嫌疑报告和引用链追踪等视图,可帮助定位大对象、异常增长的类以及无法被回收的引用路径,适合具备一定 JVM 基础的开发者用于线上问题复盘与性能诊断。包内共 463 个文件,以 jar 组件、png/gif 界面资源、html 帮助页、xml 与 properties 配置为主,另有 exe、dll、bat 等启动与运行文件,压缩包约 57.99MB,目录与依赖较完整。已有 302 人学习下载,对希望掌握 MAT 核心分析流程、理解堆转储阅读方法并积累内存问题排查思路的读者,这份工具包可作为较稳定的离线参考环境,也便于对照帮助文档熟悉各视图用途。

1. 解压一个 2016 年的 zip,换来半夜排查 OOM 的底气

凌晨两点,某公司一个后台服务被系统的 OOM 机制干掉,好在启动参数里带了-XX:+HeapDumpOnOutOfMemoryError,留下一个 4GB 的 hprof。用 jvisualvm 打开,转圈十分钟还没把对象读完;换成 Memory Analyzer(下称 MAT),索引建完、支配树一排序,retained 占 2.6GB 的那个 ConcurrentHashMap 直接排第一屏。这个 zip 就是 MAT 的 Windows 64 位构建,版本 1.6.1,打包时间戳 2016-11-25,解压即用、绿色免安装。它解决的是拿到 dump 之后定位内存占用来源的问题;抓取 dump 本身不在本文范围内。适合日常要处理 Java 服务内存问题的人,也适合手上只有一台离线 Windows 机器、dump 不能外传的场景。

2. 拆包先看结构:Eclipse RCP 目录树与 hprof 的解析链

2.1 它是一棵目录树,不是一个安装程序

解开 zip,看到的是一副很典型的 Eclipse RCP 产品形态:MemoryAnalyzer.exe、MemoryAnalyzer.ini、plugins/、features/、configuration/、artifacts.xml。.exe只是启动器(launcher),真正在干活的是plugins/下那一堆 OSGi bundle,org.eclipse.mat.*开头的几个 jar 才是分析引擎本体。

理解这一点,后面很多现象就有解释了:改内存要动 ini 而不是改快捷方式;换 JDK 要在 ini 里写-vm;启动失败时先看启动器有没有把 JVM 拉起来,再看 workspace 下.metadata/.log。几个直接后果值得先记住:

  • 同一个盘上可以并存多个版本(1.6.1 和老版本各放一个目录),互不影响,用哪个点哪个。
  • 解压路径不要带中文和空格,D:\tools\mat161这种最省事。启动器对含空格路径的处理在部分场景下会出问题。
  • 别塞进Program Files。那里受 UAC 管控,MAT 默认要在自己的 workspace 写索引和临时文件,权限问题会表现成各种莫名其妙的启动报错。
  • 整个目录拷给同事,对方机器上装好 JDK 8 就能跑,这一点在离线环境里非常实用。

2.2 hprof 里存的是图,不是一张内存快照照片

很多人第一次看 Histogram,会觉得"String 有 200 万个,那就是字符串泄漏"。这个推论经常是错的,原因在文件格式。

hprof 是二进制流,里面由若干 record 组成:字符串表、类 dump(类名、父类、字段名与类型、静态字段值)、实例 dump(类 id + 各字段值)、对象数组 dump、基本类型数组 dump、GC Root 记录(栈帧局部变量、JNI 引用、静态字段、监视器、类加载器)、线程栈片段。MAT 读完这些记录,在内存里建一张有向图:节点是对象,边是引用,根是 GC Root 集合。

三个指标必须分清:

指标含义用途
Shallow Heap对象自身占用的字节,含对象头和字段,不含它引用的对象判断单个对象大小,容易误判
Retained Heap该对象被回收后,能连带释放掉的总字节判断谁在吃内存,主看这个
Dominator从 GC Root 到某对象的路径全部经过 A,则 A 支配该对象决定 Retained 的归属关系

一个 32 字节的HashMap$Node本身没多少,但它挂着的 value 可能是个几十 MB 的 byte 数组——Shallow 看着人畜无害,Retained 才是真实的账。所以第一眼永远先看 Retained,Shallow 只在怀疑"单个大对象"(比如一个超大数组)时才拿来做二次验证。

2.3 1.6.1 这条分支能跑在哪、卡在哪

这是 2016 年底的构建,几个边界最好在动手前就知道:

JVM 基线是 JDK 8。配套的 Equinox 启动器也是那个年代的产物,用 JDK 11/17 直接去启,很常见的现象是窗口闪一下就没了,或者命令行里刷NoClassDefFoundError。这不是包坏了,是老启动器不认新 JVM 的模块化布局。手上备一个 JDK 8,是最省事的组合。

读 dump 的兼容性有代差。JDK 8 及更早版本 HotSpot 抓出来的标准 hprof 最稳。JDK 11 之后抓的 dump,用这个版本打开偶尔会中途报解析异常或者对象数对不上。真遇到了别在这里死磕,换更新的 MAT 版本去读——版本跟着 dump 的产出环境走,这条经验比记具体版本号有用。

Android 的 .hprof 要先转换。Dalvik 的 hprof 虽然也叫 hprof,头部和 record 语义不同,直接拖进去大概率报格式错误,得先用hprof-conv转成标准格式。

功能面上这个版本是齐的:Histogram、Dominator Tree、Leak Suspects、OQL、Compare Basket、HTML 报表导出都在,日常定位一个内存泄漏不需要更多东西。

3. 跑起来:解压、绑 JVM、调 -Xmx 与命令行出报告

3.1 解压位置与两种启动入口

先解压到一个固定目录,比如D:\tools\mat161,确认目录里能看到MemoryAnalyzer.exe和MemoryAnalyzer.ini。启动有两个入口,用途不同:

# 入口一:图形界面,日常排查用 D:\tools\mat161\MemoryAnalyzer.exe # 入口二:带控制台输出启动,闪退时用它看真实报错 D:\tools\mat161\MemoryAnalyzer.exe -consoleLog -console

命令行里直接双击 exe 的好处是:JVM 拉不起来、启动器加载 bundle 失败这类问题,控制台窗口不会一闪而过,报错能看全。图形界面第一次启动会让你选 workspace 目录,选一个剩余空间大的盘——workspace 里会存偏好设置和日志,小于 1GB 可用空间的盘不用考虑。

3.2 MemoryAnalyzer.ini 里必须改的三处

用文本编辑器打开MemoryAnalyzer.ini,内容大致长这样(launcher 的 jar 文件名跟着构建走,以你真解出来的为准,别照抄):

-startup plugins/org.eclipse.equinox.launcher_1.3.201.v20161025-1711.jar --launcher.library plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.401.v20161122-1740 -vm C:/Java/jdk1.8.0_202/bin/javaw.exe -vmargs -Xmx4096m -Xms1024m

三处改动,每一处都是踩出来的:

第一处,-vm和它下面那一行路径。这条是告诉启动器用哪个 JVM,必须写在-vmargs之前,路径单独占一行,不能和-vm挤在同一行。路径用正斜杠/或者双反斜杠\\,单反斜杠会被当转义吃掉。指向的必须是javaw.exe而不是java.exe——后者会额外弹一个控制台窗口挂在界面上。如果这个 JDK 是 32 位或者路径写错,启动时会报Failed to load the JNI shared library,这个报错信息跟"内存不够"看起来毫无关系,实际是位数不匹配。

第二处,-Xmx。默认值是 1024m,处理几百 MB 的小 dump 够用,上到 GB 级就不行了,典型表现是解析到一半弹An internal error occurred during: "Parsing heap dump"或者干脆Java heap space。经验值是设为 dump 文件大小的 0.5~1.0 倍之间;设置过大,GC 时间会显著增加。但注意,MAT 读 dump 用的是内存映射(mmap),不是把整个文件读进堆,所以堆不用等于文件大小——真正吃内存的是建索引和算 retained size 的过程。

第三处,-Xms。设成和-Xmx一样能减少堆反复扩张的开销,对这种"启动就猛吃内存、跑完就退出"的程序收益明显。-Xmx和-Xms中间不要留空行,-vmargs之后的内容全部原样传给 JVM。

3.3 命令行批量出报告:ParseHeapDump 的实际用法

图形界面适合边看边推断,但有两种场景更适合走命令行:一是 dump 在客户机器上、你只想拿一份 HTML 报告回来;二是要一次跑好几个 dump。MAT 自带批量脚本:

cd /d D:\tools\mat161 ParseHeapDump.bat D:\dump\app-20240115.hprof ^ org.eclipse.mat.api:suspects ^ org.eclipse.mat.api:overview ^ org.eclipse.mat.api:top_components

参数拆开看:第一个参数是 dump 的绝对路径;后面跟的是报表任务的 ID,可以有选择地给:

任务 ID产出什么时候用
org.eclipse.mat.api:suspectsleak-suspects.zip+ HTML只想快速看有没有明显嫌疑对象
org.eclipse.mat.api:overviewsystem-overview.html要给不装 MAT 的人看整体情况
org.eclipse.mat.api:top_componentstop_components.zip看最大的几个组件/类加载器

输出文件落在 dump 同目录。这里有个实用技巧:给同事的往往不是 dump 而是leak-suspects.zip解开后的那几个 HTML,对方用浏览器就能看,展开对象树也能点。JVM 参数依然由MemoryAnalyzer.ini决定,在 bat 后面追加-vmargs -Xmx8g容易被启动器吞掉,稳妥做法是直接改 ini,别在这上面省事。

3.4 索引文件:既是缓存,也是坑

第一次打开一个 dump,MAT 会在旁边生成一批索引文件,之后再用就快得多:

文件作用能否删除
<dump>.index解析元数据、对象地址索引可以,重开会重建
<dump>.o2hprof.index对象地址到文件位置的映射可以
<dump>.a2hprof.index数组对象的地址映射可以
<dump>.o2ret.index/.a2ret.indexretained size 索引,最耗时可以,但重建可能要几十分钟
<dump>.threads线程与栈帧信息可以

索引是"跟着文件名走"的。如果把新 dump 覆盖到同名旧文件上,MAT 有可能继续用旧索引,结果就是对象数对不上、点进去报错、retained 数值离谱。这种情况下一律把<dump>.*.index和<dump>.threads全删掉重来。更省心的习惯是:dump 文件名带上时间戳和进程号,永远不覆盖。

还有一个容易被忽略的点:dump 放在网络共享盘或移动硬盘上时,mmap 的随机读会慢到让人以为卡死。先拷到本地 SSD 再打开,同时确认所在分区留出 dump 大小 1/3 以上的空闲空间给索引。

4. 四个视图读出结论:Histogram、Dominator Tree、Leak Suspects、GC Roots 路径

4.1 Histogram:先用正则把噪声折叠掉

Histogram 按类名列出实例数和 Shallow Heap,默认排序其实是"按类名",很多人第一眼看到一堆java.lang.String、char[]、int[]就以为找到了,其实那只是 Java 程序的底色。

正确的用法是先在顶部的正则框里做分组。比如怀疑是业务缓存问题:

com\.example\.(cache|buffer|holder)\..*

几个要点:点号必须转义成\.,否则会匹配到任意字符;分组用括号;过滤后 Histogram 会把匹配到的所有类合并成一行显示实例总数,比逐个类翻快得多。除了正则,右键菜单里还有Group by package(按包聚合)和Group by class loader(按类加载器聚合,排查多应用部署时同一份类被加载多次特别好用)。

真正该盯的异常信号是:某个业务类的实例数明显超过业务量级(比如订单缓存说好 10 万条,Histogram 里有 400 万),或者某个基本类型数组的总 Shallow 远超其他所有类之和。

4.2 Dominator Tree:Shallow 和 Retained 差在哪

Dominator Tree 是整个工具里信息密度最高的一屏。它按支配关系组织对象,父节点的 Retained Heap 包含所有被它支配的子节点。判断"谁在吃内存",只看这一列就够了。

展开的姿势是从上往下逐层收窄:顶层通常是一个线程或类加载器,往下展开会看到Thread→ThreadLocalMap→ 你的缓存对象。每一层都看 Retained 占比,占比低于 5% 的分支直接放弃,不要在一棵小树枝上纠结。

一个典型误判:某个ArrayList的 Retained 有 1.8GB,看着像泄漏,展开发现里面装的是启动时一次性加载的字典数据,跑多久都不变。这种情况不叫泄漏,叫"常驻内存大"。区分方法很简单:隔几小时抓两份 dump 对比同一个对象的 Retained 是否持续增长。稳定不变的是容量问题,持续上涨的才是泄漏。

4.3 Leak Suspects:自动报告要带着怀疑读

Leak Suspects 是 MAT 的自动推断,打开 dump 后它会先跑一遍,生成一页 HTML。结构一般是"Problem Suspect 1/2/3",每条给出一个或一组嫌疑对象、一句描述、以及一张从 GC Root 到该对象的路径图。

读法上有讲究:先看描述里的数字是怎么来的。常见的句式是"One instance of X loaded by occupies N bytes",这句话只说明 X 这个实例的 Retained 大,没说是泄漏。如果 X 是框架里的连接池、缓存容器、类加载器元数据,大概率是正常常驻。

真正有价值的是它给出的路径图。点开Details,看 accumulated objects 那一列,能立刻看出这 N 个字节是被"单个巨大对象"撑起来的,还是被"几十万个小对象"堆起来的。前者回去查是谁分配的(用后面 4.4 的方法),后者基本就是集合只进不出。自动报告省的是找路径的时间,结论还得自己下。

4.4 Path to GC Roots:把弱引用和软引用剔掉

定位到可疑对象后,右键 →Path to GC Roots,这一步是确认"它为什么还没被回收"。

菜单里有好几个变体,日常只用两个:with all references(全量,路径可能很长很乱)和exclude weak/soft references(排除弱引用、软引用、虚引用)。默认必须选后者。原因很实际:WeakHashMap 的 key、ThreadLocal 的 Entry、各种缓存框架的软引用,在图上都是"可达"的,但 GC 真跑起来照样回收。用全量模式看路径,很容易把一条软引用链误读成泄漏,然后花半天去改一段根本没问题的代码。

读路径的方向是从下往上:最底下是你查的对象,最上面是 GC Root。中途遇到的第一个"业务代码特征"的节点,通常就是源头。最常见的两类根:

  • 线程栈上的局部变量。线程池里的线程没退出,栈帧一直挂着大对象——典型是某个任务把大 List 传进去后阻塞在 IO 上。
  • 静态字段。某个static Map只 put 不 remove,随着请求量线性增长。

5. 避坑与排查:五条踩过的记录

5.1 双击闪退,日志里是 JNI 报错

现象:双击MemoryAnalyzer.exe,窗口一闪就没了;命令行启动刷一行Failed to load the JNI shared library。

原因:ini 里-vm指向的 JVM 位数和启动器不匹配。这个包的启动器是win32.win32.x86_64,必须配 64 位 JDK;指向 32 位 JDK,或者-vm那行写成了-vm C:/...挤在一行(启动器会把整行当成一个不存在的路径),都会报这个。

解决:-vm单独一行,下一行只放路径,用javaw.exe的完整路径,确认该 JDK 是 x64。改完先在命令行里带-consoleLog启动验证,成功了再用图形界面。

5.2 解析到一半报 Java heap space

现象:进度条走到 60%~80% 停住,弹An internal error occurred during: "Parsing heap dump",或者日志里一串OutOfMemoryError: Java heap space。

原因:ini 里的-Xmx还是默认的 1024m。建索引和算 retained 是内存消耗最大的两步,几 GB 的 dump 根本不够。

解决:把-Xmx提到 dump 大小的 0.5~1.0 倍,同时-Xms设为相同值;确认机器物理内存留有余量(MAT 之外系统还要用)。改完必须完全退出 MAT 再启动,ini 只在进程启动时读一次。

5.3 新 dump 覆盖旧文件后,结果全乱

现象:把新抓的 dump 覆盖到app.hprof,打开后对象数和上次几乎一样,点某些对象报地址越界,retained 数值明显不合理。

原因:索引文件按文件名关联,旧的.index、.o2ret.index还在,MAT 认为索引有效就直接用了,读到的是两份 dump 的混合结果。

解决:删掉<dump>.index、<dump>.o2hprof.index、<dump>.a2hprof.index、<dump>.o2ret.index、<dump>.a2ret.index、<dump>.threads全部重开。根治办法是 dump 文件名永远带时间戳:app-20240115-0231-pid8823.hprof。

5.4 dump 放网络盘,加载几十分钟没动静

现象:进度条卡在 0% 或者极慢,任务管理器里 CPU 不高、磁盘队列很长,看起来像死机。

原因:MAT 对 dump 用的是 mmap 随机读,网络共享(尤其是跨机房的)随机读延迟极高,索引建不起来。

解决:拷到本地 SSD 再打开;确认分区剩余空间大于 dump 大小的 1/3(索引文件不小)。顺带一提,Windows 上 dump 文件被 MAT 打开期间是删不掉的,分析完记得关掉窗口再清理。

5.5 Leak Suspects 指向系统类加载器

现象:自动报告第一条是"One instance ofcom.example.OrderCacheloaded by<system class loader>occupies 3.1GB"。

原因:<system class loader>只是说明这个类由应用类加载器加载,和"泄漏"没有任何因果关系。真正的判断依据是这个对象的 Retained 是否随时间增长。

解决:别急着改代码。隔 30 分钟再抓一份 dump,用 Compare Basket 对比同一个类的实例数变化,涨了才是泄漏;不涨就是容量规划问题,改缓存上限或者加内存都比改代码直接。

6. 进阶:用 OQL 把结论钉死,别靠肉眼翻树

图形界面点到第五层就开始靠猜了,这时候我会切到 OQL(Object Query Language),直接把查询条件写出来。它是 SQL 的一个子集,操作对象是堆里的实例。

第一类查询:找异常大的字符串,定位拼接/缓存问题。

-- 列出长度超过 8KB 的字符串,看内容重复度 SELECT s.toString(), s.value.@length AS len FROM java.lang.String s WHERE s.value.@length > 8192

@length是 MAT 给数组类型的内置属性,s.value就是String底层的char[]或byte[]。结果表可以右键导出 CSV,拿去和另一份 dump 的 CSV 按内容对齐,重复的 key 一目了然。

第二类查询:按 retained 筛对象,比在树上翻快得多。

-- 找出 retained 超过 1MB 的集合实例 SELECT a, a.@retainedHeapSize AS retained FROM java.util.ArrayList a WHERE a.@retainedHeapSize > 1048576

@retainedHeapSize依赖 retained 索引,如果返回全是 0,先在界面上打开一次 Dominator Tree 把索引建好再跑。同一个思路换成java.util.HashMap、com.example.*Cache都成立。

还有一个我常用的组合:先把可疑包名下的类全部查出来,导出实例数和 retained,再和上一次故障留下的 CSV 并排看。人来判断趋势不可靠,数字并排放着,涨没涨一眼就清楚。这套流程读过三次线上 dump 之后,我基本不再靠 Histogram 一屏屏翻了——OQL 查出来的结果是可以贴进复盘文档、可以被别人复核的,肉眼翻树翻出来的结论只能自己信。

从那以后,我每次拿到 dump,都强制自己先跑一遍 OQL 把"最大 retained 的前十个对象"列出来存档,再开始看图。这份 1.6.1 的 Windows 64 位构建就在工具箱里躺着,命名规则一直没变,按标题里的版本号找同名压缩包,解压、改 ini、绑上 JDK 8 就能用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询