资讯动态

MemoryAnalyzer 1.6.1 实战:hprof 内存泄漏排查

发布时间:2026/10/27 2:20:19 来源:尧图企业网站定制
简介MemoryAnalyzer 1.6.120161125版是面向Java开发、测试与运维人员的堆内存分析工具包适用于排查内存泄漏、OOM、对象异常增长等性能问题。该版本为Windows平台win32.win32.x86_64构建解压后可直接运行内置Eclipse MAT的分析界面与插件依赖。压缩包共463个文件、约57.99MB以174个jar库文件、44个html帮助页、31个xml与26个properties配置、48个png及20个gif界面资源为主另含少量exe启动文件和说明文档目录结构保留完整RCP应用形态。已有302人学习下载。借助支配树、直方图、泄漏嫌疑报告和引用链分析可定位大对象持有关系与未释放引用配套说明文档便于完成安装与初次使用适合需要深入Java内存诊断的开发者作为离线工具留存。1. 一个 8G 的 hprof 摊在面前MemoryAnalyzer 1.6.1 的 win32.x86_64 包能替你回答什么凌晨两点一台线上实例的堆 dump 落地文件 8.2G业务方只丢过来一句「GC 之后内存不降」。这时候要回答的不是「有没有泄漏」这种笼统问题而是三个更具体的哪一类对象吃掉了最多的 retained heap、它是被谁攥住的、那条持有链上有没有一环本该断开却没断。Memory Analyzer 1.6.1 的 win32.win32.x86_64 包干的就是这件事——把 hprof 里那张二进制对象图翻译成直方图、支配树、GC Roots 引用路径和一份自动生成的泄漏嫌疑报告。它是 Windows 64 位的独立桌面版解压出来就能跑不需要先搭一套 IDE 环境。适合的人很明确要排查 OOM、要向别人解释内存曲线为什么掉不下来、要在上线前算清楚一个本地缓存到底占多少的 Java 后端和中间件同学。2. 解压之后先别双击目录、-vm 与 -Xmx 的三处改动2.1 目录里真正要动的只有三个文件解压得到一个 mat 目录里面几十个文件第一眼很容易懵。实际排查过程中会碰到的就那么几个其余的碰了只会给自己找麻烦路径作用要不要动MemoryAnalyzer.exeGUI 启动器启动时读同目录的 ini不动双击它就够MemoryAnalyzer.ini启动参数指定 JVM、指定堆大小必改后面两节专门说ParseHeapDump.bat命令行解析入口不出界面也能出报告不动但要知道怎么调plugins/、features/OSGi 插件本体分析能力全在这里别删删了直接起不来configuration/运行时配置缓存只在启动异常时整个删掉让它重建有一点要提前说清楚这个包是不带 JRE 的。它假设你的机器上已经有一个能用的 Java 运行时启动器会顺着 JAVA_HOME 或者 PATH 去找。所以「双击没反应」这件事九成不是包坏了是它没找到合适的 Java。先把这两件事理顺再谈分析。2.2 -vm先把 32 位 Java 排除掉Windows 上最容易翻车的地方是机器里同时装了 32 位和 64 位两个 JavaPATH 里排在前面的偏偏是 32 位那个。表现是启动器闪一下就没或者好不容易开了-Xmx 怎么调都上不去 1.5G。原因很朴素32 位进程的地址空间就那么大你给它 8G 它也没地方放。解决办法是在 ini 里把 JVM 路径钉死不要依赖环境变量。注意-vm必须写在-vmargs之前写反了不生效这是个很安静的坑# MemoryAnalyzer.ini 里只改这两处其余行保持解压原样 -vm C:/Java/jdk1.8.0_152/bin/javaw.exe -vmargs -Xmx6g路径用正斜杠或者双反斜杠都行单反斜杠会被当转义。这里写javaw.exe而不是java.exe是因为 javaw 不带控制台窗口GUI 用着干净但如果你要排查启动问题临时换成java.exe就能看到控制台输出报错信息会直接打出来比翻日志快得多。版本上1.6.x 这条线是按 Java 8 时代的运行时做的。机器上如果是 Java 8基本不用操心如果是更高版本的 JDK多数情况能起来但偶尔会碰到启动器或解析阶段的小毛病。真遇到了别纠缠把-vm指回一个 8 的 JDK问题通常当场消失。这不是让你守着老版本不放是排查工具本身不该成为排查对象。2.3 -Xmx给解析器留够堆但不是越大越好这是最多人凭感觉设错的一个参数。要点在于MAT 打开 dump 时并不是把整个 dump 读进内存它会先在磁盘上生成一套索引然后按需读取。但索引、对象图的中间结构、以及你打开的每一个视图都要占堆。所以 -Xmx 太小会在解析到一半时抛 OOM太大则可能让 GC 停顿变长、界面发卡。经验值是-Xmx 至少给到 dump 文件大小的二分之一条件允许就接近 dump 大小。8G 的 dump机器有 32G 内存就给-Xmx12g机器只有 16G给-Xmx6g也能跑只是打开支配树会慢一些得等。反过来一个 500M 的 dump 你给 16G除了让 GC 更懒之外没有任何好处。还有一处磁盘账要算解析会在 dump 同级目录生成一批同名的.index、.a1、.a2之类附属文件总体积经常和 dump 本身一个量级。所以 dump 放哪个盘不是随便选的见 2.4。提示-Xmx 的数值单位写g或m都行别写gb启动器不认会当成非法参数直接退出而且退出得毫无提示。2.4 先用命令行跑一遍确认索引能落地在双击 exe 之前我一般会先用命令行把解析跑一遍。原因有两个命令行能把错误直接打在屏幕上跑完之后索引已经生成再用 GUI 打开就是秒开省掉盯着进度条发呆的时间。# 进到 mat 目录第一个参数是 dump 路径后面是要生成的报告 id cd /d D:\mat ParseHeapDump.bat D:\dump\app-20240115.hprof ^ org.eclipse.mat.api:suspects ^ org.eclipse.mat.api:overview ^ org.eclipse.mat.api:top_components三个报告 id 分别对应泄漏嫌疑、概览、最大消费者跑完会在 dump 旁边生成几个_Leak_Suspects.zip、_Overview.zip之类的压缩包里面是可直接打开的 html。这一步的意义不只是提前出报告更重要的是验证「这个目录能不能写」。索引文件是写在 dump 旁边的如果 dump 放在只读共享盘、权限受限的目录、或者空间只剩几百兆的系统盘解析会跑到一半失败报错还相当含糊。先跑通命令行等于把这个雷提前排掉了。命令行跑完再开 GUI加载会明显快因为索引命中。如果哪天你发现同一个 dump 每次打开都要重新解析好几分钟基本可以断定索引没写成功——回去看目录权限。3. 四个视图各管一段直方图、支配树、GC Roots 路径、Leak Suspects3.1 Histogram先看数量再看 retainedHistogram 是打开 dump 后第一个该看的视图。它按类聚合每行给出这个类的实例数、shallow heap、retained heap。新手最常见的误用是上来就按 shallow heap 排序然后盯着char[]、byte[]、int[]发呆——数组类的 shallow size 求和当然最大但这不构成任何结论。正确的读法是分两轮。第一轮按Objects 列排序找「数量异常」的类一个业务对象出现几百万个实例本身就说明有问题不管它单个多小。第二轮按Retained Heap 列排序找「体量异常」的类这里才是真正占着内存不撒手的东西。两轮看完你对这个堆的形状就有概念了。视图顶部有几个分组开关值得用起来Group by package把散落在各处的类收拢到包下面看哪个模块整体占得多。Group by class loader容器环境里必备。同一个类被两个 ClassLoader 各加载一份不分组的话你在列表里会看到两个同名类很容易看漏。Group by superclass排查集合类泛滥时好用能把各种 Map 实现收拢成一行。勾选Show unreachable objects默认只统计从 GC Roots 可达的对象如果解析时保留了不可达对象勾上才能看到它们。还有个小技巧在 Histogram 某一行上右键可以直接对这批实例做「列出所有对象」「算 retained set」「找 GC Roots 路径」不用先跳去别的视图。这条右键菜单是整个工具里用得最频繁的入口。3.2 Dominator Treeretained size 才是账面上的钱shallow heap 和 retained heap 的区别是这个工具的核心概念绕不过去。shallow heap对象自身占的字节数不含它引用的任何对象。一个String大概 24 字节它指向的char[]不计入。retained heap这个对象被回收之后能连带释放掉的总字节数。关键在于「连带」的范围怎么算——只算它独占支配的那部分。支配关系的定义不复杂从 GC Roots 出发到对象 B 的所有路径都必须经过 A那么 A 支配 B。一个对象如果不是被单独持有而是被好几个地方同时引用那它就不完全由某一个持有者支配retained size 会被分摊。这个定义解释了一个很多人困惑的现象把一个缓存对象删掉内存并不一定降下它 retained size 那么多因为它引用的东西别处还拿着。Dominator Tree 就是按支配关系排出来的一棵树父节点的 retained size 一定大于等于子节点。看这棵树的方法是从上往下走先看根下面那几个大头是谁如果是Thread那是线程栈相关如果是某个ClassLoader是类元数据如果是业务侧的连接池或缓存容器那就有戏了。一路展开到能叫出业务名字的类为止通常不超过五层。提示支配树的计算代价不低超大 dump 上第一次展开可能要等十几秒。别在这期间反复点刷新越点越慢。3.3 Path to GC Roots为什么它还没被回收找到可疑对象之后下一个问题永远是「谁在引用它」。在任意对象上右键 → Path to GC Roots →exclude weak/soft references这是默认动作也是必须的动作。原因在于WeakReference、SoftReference这类引用不阻止回收。如果一个对象只是被软引用挂着它其实随时可以被回收不构成泄漏。不排除这些引用你会看到一棵挂着几十层SoftReference→ReferenceQueue→ 内部结构的树全是噪音。排除之后剩下的引用链才是真正把对象钉在内存里的那几根钉子。引用链读法是从下往上最底下是你的对象往上每一层是持有者一直到Thread或ClassLoader这样的根。常见的几条典型链路链路形态通常意味着Thread→ThreadLocalMap→ 业务对象ThreadLocal 用完没 remove线程池里线程不死对象就一直活着ClassLoader→classes→ 静态字段 → 容器静态集合只增不减典型的本地缓存没有淘汰Thread→ 线程池Worker→BlockingQueue→ 任务对象队列积压任务对象里带着大 payloadClassLoader→ 某个监听器列表 → 业务对象注册了监听器但没反注册容器销毁后整棵图都没释放这条链看完基本就知道该去改哪一行代码了。3.4 Leak Suspects能省半小时但不能当结论Leak Suspects 是一份自动生成的报告用一组启发式规则扫一遍堆把「retained size 占比最高的若干支配者」「大量重复字符串」「finalizer 堆积」「空集合泛滥」之类的模式挑出来配一张饼图和几段文字描述。它的价值在于快。一个刚拿到的陌生 dump先看这份报告能在几分钟内建立大致印象知道该往哪个方向挖。它的局限在于启发式终究是启发式它不认识你的业务。一个占 40% 内存的HashMap报告会标红但那可能就是你设计上要常驻的本地缓存完全正常反过来一个只占 3% 但持续增长的泄漏报告里大概率一笔带过。所以我的用法是把 Leak Suspects 当目录不当判决书。从它标出的可疑点进去用 3.1 到 3.3 那三套视图自己核实一遍确认「这个对象的持有链确实不该存在」才算数。报告的每一个 suspect 都可以点进去看细节点进去之后走的还是直方图和支配树那一套绕不开的。4. OQL 与快照对比把「看着像」变成可验证的数字4.1 OQL 的三条基本骨架OQL 是 MAT 内置的对象查询语言语法长得像 SQL查的是堆里的对象图而不是表。视图入口在工具栏的 OQL 面板写完之后按执行结果以表格形式出来。绝大多数排查场景用三种骨架就够了-- 骨架一按类过滤附加字段条件 SELECT s FROM java.lang.String s WHERE s.count 200 -- 骨架二算某个类独占持有的对象集合 SELECT AS RETAINED SET * FROM com.example.cache.LocalCache -- 骨架三按引用类型捞确认缓存挂在哪种引用上 SELECT * FROM java.lang.ref.SoftReference第一条里s.count是String内部的字符数量字段能直接访问私有字段是 OQL 的一个便利之处不用反射。第二条的AS RETAINED SET是关键语法它返回的不是匹配到的对象本身而是这些对象支配的整个集合用来估算「清掉这个缓存能省多少」非常直接。第三条用来区分「内存被软引用占着」和「内存被强引用钉死」这两种情况的处理方式完全不同——前者调-XX:SoftRefLRUPolicyMSPerMB或者干脆不管后者必须改代码。注意Java 9 之后String的内部表示从char[]改成了byte[]加一个编码标志位。用这个版本的 MAT 去打开新 JDK 产生的 dump字段名和类型可能对不上s.value之类的查询会报类型不匹配。遇到这种情况别硬改查询从byte[]那一侧反查更快先 Histogram 定位byte[]再对可疑的那批对象右键找 GC Roots 路径。4.2 几个能直接抄的查询排查中最常敲的几类查询整理成可以直接改包名就用的形式-- 找超长字符串定位拼接出来的大 payload SELECT s, s.value FROM java.lang.String s WHERE s.count 5000 -- 找没被清理的 ThreadLocal 条目值为 null 但 key 还在的残留 SELECT * FROM java.lang.ThreadLocal -- 找队列里堆积的任务按集合大小排序 SELECT * FROM java.util.concurrent.LinkedBlockingQueue q WHERE q.count 1000第二条需要说明一下ThreadLocalMap的 Entry 是弱引用 keykey 被回收后 value 还挂在表里形成「key 为 null 的僵尸 entry」。这类残留不会自己消失线程池里的线程活得越久积得越多。用 OQL 把ThreadLocal全捞出来再结合引用链看是哪个业务代码注册的比盲猜快。第三条里q.count是LinkedBlockingQueue内部记录元素个数的字段。同类的还有AtomicInteger的value、各种HashMap的table。写 OQL 之前先看一眼类的字段定义比试错快。4.3 两个 dump 做对比Compare Basket 的用法单看一个 dump 只能知道「现在占多少」看不出「什么在涨」。真正的泄漏判断往往需要两个时间点的快照做差。Compare Basket 就是干这个的操作路径如下在同一个 MAT 会话里先后打开两个 dumpFile → Open Heap Dump选第二个文件时不要关掉第一个。在第一个 dump 的 Histogram 上右键选Add to Compare Basket。切到第二个 dump同样在 Histogram 上右键Add to Compare Basket。打开 Compare Basket 视图点右上角的感叹号按钮执行对比。结果是一张三列的表类名、快照一的实例数、快照二的实例数可以按差值排序。涨得最凶的那几行就是嫌疑最大的。这个功能有几个前提条件不满足的话结果会很误导人。两个 dump 必须来自同一个进程的连续运行中间不能重启一重启所有对象地址和类加载器 id 全变了对比出来的差值毫无意义。另外两次采集的时机要选好都在 Full GC 之后采否则差值里混着大量还没回收的临时对象噪音会盖过信号。4.4 把结果导出再做二次聚合MAT 的表格视图能做的事有限很多时候需要把数据拿出来用别的工具算。Histogram 右键 → Export → CSVOQL 结果右键 → Export都能导出。导出的 CSV 里保留了类名、实例数、shallow、retained 四列用表格软件按包名做一次透视很容易看出「哪个模块在这次发版之后内存涨了」。我习惯在排查结束后把关键视图都导一份存档。原因很现实dump 文件太大通常保留不了几天就得删但那张 CSV 只有几十 KB留半年都没问题。下一次同一个服务再出问题把两份 CSV 放一起看比重新打开两个 8G 的 dump 快得多。5. 避坑与排查五个在真实现场反复出现的翻车点5.1 双击 exe 一闪而过什么都没留下现象双击MemoryAnalyzer.exe任务栏图标闪一下就没了没有任何对话框事件查看器里也看不出所以然。原因启动器找不到合适的 JVM或者找到了一个 32 位的。这个包不捆绑运行时它对环境是有要求的。有时候是 JAVA_HOME 指向了一个已经卸载的路径有时候是 PATH 里排前面的是 32 位版本。解决把 ini 里的-vm显式指向一个 64 位 JDK 8 的javaw.exe写法见 2.2临时把javaw换成java跑一次看控制台报错。还有一种更隐蔽的情况路径里有中文或空格某些版本的启动器处理不好把 mat 目录挪到纯英文短路径下再试。5.2 解析卡在某个百分比不动或者报索引创建失败现象进度条走到 60% 左右停住硬盘灯不闪等十分钟也没动静或者直接弹窗提示无法创建索引文件。原因索引是写在 dump 同级目录的。dump 放在只读的网络共享盘、权限受限的目录、或者剩余空间不足的盘上都会在这里炸。另一个原因是 dump 本身不完整——采集过程中进程被 kill、磁盘写满文件头看着正常尾部是截断的。解决先把 dump 拷到本地 NTFS 盘的一个普通目录确认剩余空间至少是 dump 大小的两倍再重新解析。如果换了目录还是失败去比对一下文件大小和采集时日志里报的大小对不上就是 dump 截断了这份文件没救只能重新采。5.3 -Xmx 一路加到 16g打开还是 OOM现象明明机器有 64G 内存ini 里-Xmx16g也写了解析到一半还是抛OutOfMemoryError: Java heap space。原因两个可能。一是-Xmx压根没生效因为写在了-vmargs前面或者 ini 里存在重复的-Xmx行、后一条覆盖了前一条。二是启动器用的是一个 32 位 JVM堆上限被地址空间卡死你写多少它都只能给到 1.5G 左右。解决打开 GUI 之后看 Help → About 里显示的 JVM 和最大堆一眼就能确认参数有没有吃进去。没吃进去就回头检查 ini 的位置和重复行显示的最大堆远小于设定值就是 32 位 JVM 的问题回到 5.1 处理。5.4 Histogram 里全是 char[]、byte[]找不到业务类现象按 shallow heap 排序前二十行清一色是基础类型数组和java.lang.String翻好几页都看不到自己的包名。原因数组和字符串是所有业务的底层载体按占用排序它们必然在前面。这不是工具的问题是看法的问题。解决换成按 Objects 列排序找数量异常或者用 Group by package 把视角抬到模块层。真要抓具体对象用 3.1 里说的右键入口从可疑的数组行直接「列出所有对象」再顺着引用链往上爬两层业务类就露出来了。5.5 明明怀疑泄漏Path to GC Roots 却显示对象可回收现象Histogram 里某类对象数量在涨但对着其中一个实例找 GC Roots 路径结果只有一条弱引用链看起来随时能被回收。原因采集 dump 时用了jmap -dump:live这个参数会先触发一次 Full GC 再 dump那些本来在泄漏、但当时恰好没人引用的对象已经被回收掉了自然看不到。用 live 参数采到的快照是「GC 之后的内存」用它判断泄漏等于把证据先销毁了。解决怀疑泄漏时用不带 live 的jmap -dump:formatb,filexxx.hprof pid把不可达对象也留在快照里。代价是文件更大、里面有垃圾对象干扰但排查泄漏要的就是完整现场。同时确认 MAT 解析时保留了不可达对象Histogram 里勾上 Show unreachable objects 再看一次。6. 把它接进日常三件固定下来的小事工具本身没什么神秘的难的是让它在需要的时候真的能用上。我后来固定做了三件事。第一件把 dump 采集参数写进所有服务的启动脚本不靠人临场敲命令java -Xms4g -Xmx4g \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/dump \ -jar app.jarHeapDumpPath一定要单独指到一个空间充裕的盘。默认它写在进程工作目录下很多 OOM 现场就是因为工作目录所在的分区满了dump 写到一半失败最后什么线索都没留下。第二件dump 文件命名带上服务名、时间戳和触发原因比如order-svc_20240115-0231_oom.hprof。听起来是小事但目录里堆了十几份java_pid1234.hprof的时候你就知道这条规范值多少钱。第三件也是最容易忽略的一件把 ParseHeapDump 的命令行固化成一个脚本每次拿到 dump 先跑一遍批量报告再开 GUI 细看。这里我把常用的几个报告 id 列一下报告 id产出内容什么时候用org.eclipse.mat.api:suspects泄漏嫌疑报告每次必跑用来建立第一印象org.eclipse.mat.api:overview堆概览、大对象汇总交接给别人时附上org.eclipse.mat.api:top_components最大消费者列表快速定位几个大头org.eclipse.mat.api:system_properties系统属性快照确认 JVM 参数和文件编码修复上线之后我会再做一次同样的流程压测跑起来采一份新 dump用 Compare Basket 和修复前那份做差看目标类的实例数是不是真的降下来了。这一步不能省——代码改了不代表对象就少了可能是从 A 集合挪到了 B 集合数字会告诉你真相。从那以后我每次拿到 dump都强制先走一遍「命令行解析 → Leak Suspects 看方向 → Histogram 两轮排序 → 支配树落地 → 引用链确认」这五步不跳步也不靠第一眼的直觉下结论。吃过一次亏当时盯着一个占比 30% 的HashMap查了两天最后发现真正的泄漏是另一个只占 2%、每十分钟涨一点的ThreadLocal残留。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑