多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MAT内存分析工具Windows版:从原理到实战排查OOM

MAT内存分析工具Windows版:从原理到实战排查OOM 简介MemoryAnalyzerMAT是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.1JDK 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 到 OQL4.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 referencesMAT 会展开一条从 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 位 JREMAT 启动器拉起的是 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 前都会先看一眼磁盘余量。希望帮到你。本文还有配套的精品资源点击获取
返回列表