简介:MemoryAnalyzer(MAT)是Eclipse基金会推出的开源Java堆内存分析工具,这款1.6.1版本压缩包构建于2016年11月25日,面向Windows 32/64位环境,主要帮助Java开发者、运维人员与性能调优工程师排查内存泄漏、分析堆转储文件,定位应用卡顿或内存溢出根因。压缩包共463个文件,整体约57.99MB,内含可直接运行的MAT主程序、启动脚本及dll运行库,同时集成大量jar组件、HTML帮助页面、XML与properties配置资源,以及png/gif图形界面图标,解压后即可在图形化环境中快速浏览内存占用概况。已有301人学习下载,适合具备一定JVM基础、希望系统掌握对象引用路径和垃圾回收行为的读者。工具提供Dominator Tree支配树、Leak Suspects自动报告、Histogram统计和引用链追踪等分析视图,可辅助发现未关闭的连接、静态集合持有等典型泄漏源;包内说明.txt附有安装和基础使用指引,结合自己的堆转储文件实践,能显著提升内存诊断效率与Java应用稳定性。
1. 这个压缩包不是源码,是 Java 内存排查的常用工具 MAT 的 Windows 发行版
一个 Java 服务跑了两周,内存从 80% 爬到 90%,某天凌晨直接 OutOfMemoryError,告警响成一片。重启后一切正常,但两天后曲线又往上爬。光看 GC 日志已经回答不了“到底谁占着内存不放”,你要做的是抓一份堆转储,再用一个能啃得动它的工具打开。标题里这个压缩包,就是 Memory Analyzer Tool 的 Windows 64 位发行版,版本 1.6.1,构建日期 2016 年 11 月 25 日,解压即用。
这里容易看走眼:文件名里的 win32 不是“32 位程序”,那是平台沿用多年的命名习惯,真正决定架构的是末尾的 x86_64。它适合正在排查 OOM、内存泄漏、GC 压力过大的一线后端、运维和中间件开发者。下面我从版本拆解开始,把它的原理、启动参数、分析套路和踩坑点一次讲清楚。
2. 读懂 1.6.1.20161125-win32.win32.x86_64:版本、平台与内存分析原理
2.1 版本号拆解:构建日期、平台标识与 64 位架构判断
版本号里每个字段都有实际含义。MemoryAnalyzer 是工具名,1.6.1 是主版本号,20161125 是构建日期,也就是 2016 年 11 月 25 日打出来的包。中间的 win32.win32 是工具所属平台对 Windows 图形环境的固定标识,最后一段 x86_64 明确告诉你这是 64 位架构的程序。
为什么要花时间讲这个:我在实际排查中遇到过同事下载了 x86 版本,在 64 位 Windows 上打开 2GB 以上的转储,MAT 自己的堆撑爆,误以为工具不行,其实只是 JVM 地址空间上限卡在 1.5GB 左右。看到 x86_64 就可以放心用大堆参数。另外 1.6.1 这个版本的运行环境是 JDK 8,它在 JDK 8 上表现最稳,而这恰好是目前存量生产环境覆盖率很高的 JDK 版本。如果你的服务已经升级到 JDK 17,才需要考虑迁移到新版本 MAT。
2.2 MAT 的核心算法:Shallow Heap、Retained Heap 与支配树
MAT 能在一个巨大的 hprof 文件里快速找出内存大头,靠的不是暴力遍历,而是几个经典概念。Shallow Heap 指对象自身占用的内存,不含它引用的其他对象;Retained Heap 指如果这个对象被回收,整个堆能释放出的总内存,等于对象自身加上只有通过它才能到达的那些对象。分析内存泄漏,真正要盯的是 Retained Heap。
举个例子:一个 HashMap 里有 10 万个 Entry,每个 Entry 又引用着键和值。HashMap 对象自己的 Shallow Heap 可能只有几十 KB,但它的 Retained Heap 可能是几百 MB。如果你只按 Shallow Heap 排序,这个罪魁祸首永远排在后面。所以 MAT 的默认排序都是按 Retained Heap 来的,这不是没道理的。
Dominator Tree 是另一个关键算法。如果从 GC Roots 出发,所有到达对象 B 的路径都必须经过对象 A,就说 A 支配 B。把这种支配关系画成树,树根附近就是内存占用的源头。MAT 的 Dominator Tree 视图帮你直接看到“谁支配着最大的那一坨对象”,配合 Path to GC Roots 就能沿着引用链追到根持有者。
GC Roots 指 JVM 里那些不会被回收的起点,包括静态字段引用、活跃线程栈帧里的局部变量、JNI 引用等。从嫌疑对象一路往回找,最终都会落到某个根上。大部分内存泄漏的本质,就是某个本该被清理的对象,被一个长期存活的根对象无意中引用着,导致整棵引用子树都无法回收。
MAT 打开 hprof 时,也不是把原始文件一次性读进内存,而是先建立对象索引和类索引,再按需加载。这就是为什么大文件第一次打开特别慢,第二次会快一些,因为索引文件被缓存了。理解这一点对设置 MAT 自身堆大小很有帮助:解析复杂引用关系时,索引阶段消耗内存最大,所以堆参数不能按转储文件大小随便给个值了事。
2.3 为什么还在用 1.6.1:JDK 8 存量环境的现实选择
新版本 MAT 界面更现代,对新 JDK 的转储支持也更好,但有一个现实问题:新版要求更高的 JDK 运行环境,而很多生产服务器上跑的还是 JDK 8,排查机器上也不一定装得了新 JDK。1.6.1 安装包体积小、启动快、在 Windows 上字体渲染也正常,对 JDK 8 生成的堆转储解析完全没问题,所以成了很多团队留在身边的版本。
从兼容性边界来看:1.6.1 分析 JDK 8 的转储体验最好;如果你的业务已经迁到 JDK 11 以上,字符串内部表示从 char[] 变成了 byte[],老版本的某些展示会有出入,这时建议换新版 MAT。选版本的原则很简单:转储来自哪个 JDK,就用那个时代对应的 MAT 版本,不要盲目追新,也不要死守旧版。
还有一个常见误用是拿 MAT 当监控工具,每天定时抓转储做全量分析。这没有必要。转储分析是事后取证手段,不是实时监控手段。实时监控靠 GC 日志、堆使用率曲线、对象创建速率这些更轻量的指标来承担。MAT 的价值在于:当这些指标都指向“老年代持续上涨”而你无法解释时,它给出最终答案。
3. 在 Windows 上把 MAT 跑起来:解压、内存参数与两种启动方式
3.1 环境确认与解压目录的约定
先把环境确认清楚,省得后面翻车。拿到压缩包后,先确认系统里装了 64 位 JDK,版本最好在 JDK 8 附近。我一般会执行一条命令看版本,确认路径里没有被指向 32 位 JRE。
java -version输出里如果能看到 64-Bit,说明环境没问题。接下来解压,解压目录有两个约定:路径里不要有中文,不要有空格。比如 C:\tools\mat 就比 C:\Program Files\MAT 省心,因为有些脚本对带空格的路径处理不够友好。解压完成后,目录里应该能看到 MemoryAnalyzer.exe 和 MemoryAnalyzer.ini 这两个关键文件。
解压完成后先别急着双击。进目录看一眼,除了 MemoryAnalyzer.exe,还能看到 MemoryAnalyzer.ini 和一堆 plugins、configuration 目录。程序默认的工作区在用户目录下,里面放中间索引和分析缓存。如果你同时分析好几份转储,这个目录会越来越大,建议在启动脚本或配置文件里把工作区指到磁盘余量更充足的位置。这个配置不是必须,但长期用 MAT 的人都会改,否则某天磁盘突然满了,你都不知道是谁干的。
3.2 修改 MemoryAnalyzer.ini:堆参数的设置
MAT 自身也是一个 Java 程序,解析 hprof 时它要把对象图加载到自己的堆里。默认的 -Xmx1024m 只够分析小转储,遇到 GB 级的转储几乎必挂。我一般按转储文件大小的 1.5 到 2 倍来设置 MAT 的堆,比如转储 2GB,就给 4096m;转储 4GB,就给 6144m;机器内存不够时,优先保证 MAT 的堆,浏览器和其他程序能关就关。
打开 MemoryAnalyzer.ini,用文本编辑器改,常见配置长这样:
-vmargs -Xms2048m -Xmx4096m -XX:+UseParallelGC逻辑说明:-Xms 和 -Xmx 分别控制 MAT 自身的初始堆和最大堆,两个值设成一样可以减少运行中堆扩容导致的停顿。-XX:+UseParallelGC 是并行回收器,解析大对象图时吞吐量更好。参数说明:如果分析的是 8GB 的转储,-Xmx 建议 12288m 以上,并且确保机器物理内存留有余量;64 位程序不存在 32 位 1.5GB 的上限,但受物理内存约束。
3.3 启动与堆转储生成:jmap 命令与图形界面
配置改好后,双击 MemoryAnalyzer.exe 就能进图形界面。没有图形界面的服务器上,也可以用命令行 headless 模式直接导出分析报告。解压目录里带的 ParseHeapDump 脚本在 Windows 下是 .bat 文件,常见用法是:
ParseHeapDump.bat heap.hprof org.eclipse.mat.api:top_components逻辑说明:第一个参数是堆转储文件路径,第二个参数是报告类型。org.eclipse.mat.api:top_components 是 TOP 组件报告,缩写是 top_components,能快速给出嫌疑对象列表。参数说明:如果只想生成 Leak Suspects 报告,把第二个参数换成 org.eclipse.mat.api:suspects,也就是别名 suspects,两条命令适合不同的排查阶段。
转储文件从哪来,我一般用 jcmd 而不是 jmap,因为 jcmd 在同一版本 JDK 上行为更一致。先 jps 查出进程号,再执行:
jcmd <pid> GC.heap_dump /data/heap.hprof逻辑说明:jcmd 的 GC.heap_dump 会触发一次堆转储,生成的文件是标准 hprof 二进制格式,MAT 直接能打开。参数说明: 是 Java 进程号,用 jps -l 可以查到完整启动类;如果只用 jmap,可以加 live 参数只导出存活对象,但 live 会先触发 Full GC,生产环境慎用。个人经验是完整转储更值得分析,因为泄漏对象往往藏在“你以为已经回收”的那部分里。
4. 定位内存泄漏的标准动作:从 Leak Suspects 到 OQL
4.1 加载堆转储与初次分析
打开 MAT 后,把 hprof 文件拖进窗口,或者通过 File 菜单里的 Open Heap Dump 选择文件。加载时会弹出一个解析确认框,通常选默认的 Leak Suspects Report 即可,它会引导你分析。第一次解析大文件会明显卡顿,这是正常现象,MAT 正在生成索引。
加载完成后进入 Overview 页面,会列出几个关键视图的入口:Histogram、Dominator Tree、Leak Suspects、Top Components。我的建议是不要急着点 Leak Suspects,先看一眼 Overview 里的基本信息,确认转储来自哪个 JVM、总堆大小、类数量,这些信息后面和 GC 日志对得上。
一个值得养成的习惯是:加载完成后马上把原始 hprof 文件备份一份,再开始分析。MAT 会在原文件同目录生成 .index、.ooc 等中间索引文件,大小可能接近原文件。分析完这些索引不会自动清理,磁盘空间小的话容易莫名其妙爆掉,后面避坑章节还会再提到。
4.2 Leak Suspects 报告:快速锁定最大嫌疑
Leak Suspects 是 MAT 最有名的入口,它把堆里的对象按“疑似泄漏”排序,给出嫌疑报告。进入报告后,第一屏会列出排名靠前的几个嫌疑组件,每个都标了大小和占比。点开任意一条,能看到三个核心信息:Shortest Paths To the Accumulation Point、Accumulated Objects in Dominator Tree、堆栈快照。
Shortest Paths 就是从 GC Root 到嫌疑对象的最近引用路径,这是判断“谁引用着它”的关键。Accumulated Objects 展示支配树里的累积占比,告诉你这个问题有多大。堆栈快照则是抓取转储那一刻的调用现场,往往能直接看出分配点。这三块信息组合起来,大多数缓存泄漏都能当场破案。
要注意,Leak Suspects 本质是启发式判断,它擅长抓“缓存类”泄漏,比如一个静态 Map 只进不出,或者一个 ThreadLocal 没清理。但它对以下情况不敏感:直接 ByteBuffer 分配的堆外内存、线程栈占用、ClassLoader 泄漏速度极慢的场景。所以 Leak Suspects 是起点不是终点,它能让你在几分钟内锁定方向,但最终定论要结合支配树和 OQL 交叉验证。
4.3 Dominator Tree:顺着支配树找到根持有者
Dominator Tree 视图是排查泄漏的主力。打开后默认按 Retained Heap 从大到小排。我的操作顺序是:先看前 5 行的类名,判断是业务对象还是容器对象;如果看到大块的 char[] 或 byte[],说明是字符串或字节数据堆积;如果看到业务相关的 Service 或 Cache 类,八成是静态引用没释放。
然后对嫌疑对象右键,选择 Path to GC Roots 里的 with all references,MAT 会展开一条从 GC Root 到该对象的完整引用链。这条链就是“谁抓着你不放”的铁证。顺着链看,通常会发现一个全局缓存、一个监听器注册、或者一个线程池队列。到这里,泄漏点基本已经水落石出。
在 Dominator Tree 里还可以利用过滤器缩小范围。按 Class 名称过滤业务类,按包名前缀过滤第三方库对象。比如怀疑是某个 http 客户端连接池泄漏,就过滤对应包名下的类,再对结果排序。这个过程比全局翻找快得多,也不容易被无关大对象干扰。
4.4 OQL 查询:把特定对象捞出来做交叉验证
OQL 是 MAT 内置的对象查询语言,语法和 SQL 类似,但面向的是 Java 对象图。当你需要精确回答“有多少个这种对象”或“这个对象的字段值是什么”时,OQL 比图表更直接。在 MAT 的 OQL 控制台里,可以直接执行查询。
SELECT * FROM java.lang.String WHERE length > 100000逻辑说明:这条查询找出所有长度超过 10 万的字符串对象,适合排查超长报文、大 JSON 拼接问题。参数说明:属性名严格区分大小写,length 是 String 对象在 JVM 里的属性名;如果你不确定字段,先执行 SELECT * FROM java.lang.String 看结构,再改条件。
另一个高频率用的查询是按类型统计数量:
SELECT COUNT(*) AS cnt, className FROM java.lang.Object GROUP BY className ORDER BY cnt DESC逻辑说明:对所有对象按类型分组统计,结果能告诉你哪类对象数量最多,配合 Histogram 的 Retained Heap 就能区分“数量多但体积小”和“数量少但体积大”两种泄漏模式。参数说明:className 是 MAT 为每个类自动生成的隐藏属性,OQL 里可以直接用,不需要反引号。
OQL 相比 SQL 最大的不同是支持用 * 号导航对象引用。比如:
SELECT * FROM java.util.HashMap$Entry e WHERE e.key.length > 50逻辑说明:通过 e.key 访问 Entry 的键对象,再取键的长度,这在 SQL 里要 join,在 OQL 里直接走对象引用。参数说明:HashMap$Entry 的写法是内部类在 JVM 里的全名,$ 是嵌套类分隔符,这个符号在 OQL 里不能换成点号。掌握这三个查询,大部分内存问题的交叉验证已经够用。
5. MAT 高频问题排查:Windows 环境下的 5 个真实踩坑记录
这一章列了五个我在 Windows 环境里反复踩过的坑,每条都按现象、原因、解决三个步骤写,你不用重走一遍弯路,直接对号入座。有两条看起来像 MAT 本身的问题,最后查到底其实是环境或抓取方式导致的,这类问题在实际排查里最迷惑人。
5.1 打开大转储报 Java heap space
现象:加载 2GB 以上的 hprof,进度条走到半程,弹 Java heap space,然后工具退出,再打开同一个文件还是一样,连错误日志都看不到。
原因:MAT 自身的 JVM 堆不够,默认 -Xmx1024m 只够小文件,解析大对象图时堆被打满,抛出 OutOfMemoryError。很多人误以为转储有多大,MAT 就能打开多大,其实 MAT 解析过程需要额外的堆空间,索引和引用关系计算的消耗往往超过原始文件体积。
解决:编辑 MemoryAnalyzer.ini,把 -Xmx 设到转储大小的 1.5 到 2 倍,-Xms 设成相同值;机器内存不够时,用 gzip 压缩转储文件再打开,MAT 支持直接读取 .gz 格式的 hprof,索引体积也会减小;或者改用 headless 模式在服务器上导出报告,再把报告拿回本地看。
5.2 生成 Leak Suspects 报告时长时间卡死
现象:分析进度条停在某个百分比不动,CPU 100%,等十分钟也没反应,看着像死机。
原因:Leak Suspects 计算的是全局支配树,对象引用图复杂时耗时会指数级上升;另一个常见干扰因素是 Windows Defender 实时扫描 MAT 的临时索引目录,导致大量 IO 停顿。这两个因素叠加时,体验非常糟糕,很多人直接关掉进程然后放弃 MAT。
解决:先等 5 到 10 分钟观察 CPU 是否持续工作,不要急着杀进程;把 MAT 的工作区目录加到杀毒软件排除列表;如果还是慢,放弃 Leak Suspects,直接开 Dominator Tree,它的计算更快,结果也更能说明问题。
5.3 OQL 查询返回空或字段名带红叉
现象:照着别人文章写的查询执行,返回空,或者字段名上有红叉,双击看不到值。
原因:MAT 的 OQL 对属性名是大小写敏感的严格匹配。JDK 8 里 String 的 value 是 char[],JDK 9 之后是 byte[],字段名不同;另外有些字段在 MAT 内部被标记为隐藏,直接显示红叉,不代表数据丢失。
解决:先执行 SELECT * FROM 某个类,不带任何条件,看返回对象的字段列表,再照着实际字段名写条件;不要直接抄网上的查询,版本对不上就翻车。更稳的写法是用 * 号导航对象引用,先确认对象结构再下结论。
5.4 导入转储后所有线程栈都显示 Suspended
现象:打开 Thread Overview 或线程详情,所有线程状态都是 Thread [main] (Suspended),看起来像所有线程都卡死了。
原因:这不是 MAT 的问题,而是转储的抓取方式决定的。jmap -dump:live 或 jcmd 抓堆转储时会先暂停所有线程、进入安全点,抓到的线程栈天然是全暂停状态。
解决:分析内存时,Suspended 状态不影响对象引用的定位,放心使用。如果你要分析的是线程运行状态,用 jstack 单独抓一份线程栈,不要从 hprof 里看。两者用途不同,不要混着下结论。
5.5 双击启动时报 Failed to create the Java Virtual Machine
现象:双击 MemoryAnalyzer.exe,窗口一闪而过,弹 Failed to create the Java Virtual Machine,命令行里看得更清楚。
原因:典型三个来源。一是 -Xmx 设置超过物理内存;二是系统 PATH 指向了 32 位 JRE,MAT 启动器拉起的是 32 位 JVM,堆上限被卡在 1.5GB 以下;三是 .ini 里 -Xmx 写法不对,比如写成 4g,部分版本不认这个单位。
解决:把 -Xmx 调回物理内存范围内,用 java -version 确认 PATH 里是 64 位 JDK;如果系统同时装了多个 JDK,把环境变量指到明确版本。改完 ini 重新双击,问题通常当场消失。如果还不行,删除 .ini 按默认启动,确认工具本身没问题后再逐步加参数。
6. 进阶:把 MAT 用成常态化内存巡检工具
6.1 同一服务两个时间点的转储对比
泄漏排查需要证据链,单张快照只能证明“现在堆里有什么”,不能证明“哪些东西在变多”。我一般会在服务刚启动的稳定期抓一份基线转储,运行 24 小时后再抓一份对比转储。两个文件用两个 MAT 实例打开,都进 Histogram,把对象数量按类对齐,差值最大的类就是增长源。也可以直接看两份 Leak Suspects 的排名变化,排名稳定上升的组件优先查。
6.2 和 GC 日志联动
GC 日志告诉你“老年代在涨”,MAT 告诉你“老年代里是谁”。建议在 JVM 参数里提前配上 -XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath,线上 OOM 时自动落盘,这样不需要现场复现,事后拿着 hprof 就能分析。这是性价比最高的兜底方案,很多团队把它当成发布配置的标准项。
6.3 服务器上的 headless 批处理
把 ParseHeapDump 脚本和计划任务绑在一起,可以做到每周自动对一份转储生成报告,再集中下载到本地做对比。我的日常习惯是:抓到堆转储后,第一时间压缩打包,再下载到本地分析,分析完删掉中间索引,既保留证据又不污染磁盘。这个习惯源自一次真实教训——某次分析完忘了删索引,原文件加索引差点撑爆整个数据盘。那次之后,我每次打开 MAT 前都会先看一眼磁盘余量。希望帮到你。
本文还有配套的精品资源,点击获取