尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java堆内存泄漏排查:hprof、MAT索引与Retained Size实战
简介面向 Java 开发与性能调优人员的 MATMemory Analyzer Tool离线工具包围绕 hprof 堆转储文件的解析与内存问题排查展开可用于定位内存泄漏、内存占用过高及对象引用关系异常。压缩包共 2000 个文件约 75.44MB以 html 帮助文档、png 与 gif 界面截图、jar 插件及依赖为主辅以 xml 配置、dita 说明、css 样式和 properties、class 等运行文件并附带示例 hprof 数据便于离线查阅与直接试用。已有 6621 人学习下载。包内文档覆盖支配树、直方图、泄漏嫌疑人报告、浅堆与保留堆、OQL 查询、线程堆栈等分析视图配合截图和示例数据可快速理解各功能入口与判读思路jar 与启动脚本也方便在本地完成安装配置适合需要反复练习堆内存分析、对照排查真实 OOM 问题的开发者使用。1. 一个 3GB 的 hprof 拖进 MAT 就崩问题从来不在堆大小线上某服务的堆内存曲线连着三天只涨不跌运维同事丢过来一个 3.2GB 的 hprof 文件说“你分析下”。我双击 MemoryAnalyzer 的启动图标转了两分钟进度条屏幕上弹出An internal error occurred during: Parsing heap dump错误信息里夹着一句Java heap space——分析工具把自己给 OOM 掉了。这事听着荒诞但凡是拿 mat 工具啃过大堆的人大概率都踩过。MAT 不是“打开文件看表格”的记事本它是一个把 hprof 二进制流重建成完整对象图、再在自身 JVM 堆里跑支配树算法的重型分析器它自己也吃内存。这篇从 dump 怎么触发、索引怎么生成、四个视图怎么读数一路写到泄漏链路怎么定位和几个能让结论直接翻车的参数坑。2. hprof 从哪来dump 触发方式、文件结构与 MAT 索引链路分析之前先得有一份“干净”的 hprof。这一步很多人随手一条命令就过去了后面所有结论的可信度其实都押在这里用了live参数的和没用的dump 出来的是两个世界OOM 现场自动落盘的和事后手动补一刀的看到的对象状态也完全不同。先把来源这件事讲清楚再谈 MAT 怎么解析。2.1 三种 dump 方式的参数差异与代价常见做法是三种。第一种是jmapJDK 8 时代最顺手# 不带 livedump 整个堆包括已经不可达但还没被回收的对象 jmap -dump:formatb,file/data/dump/heap_$(date %s).hprof 12345 # 带 live先强制触发一次 Full GC只 dump 存活对象 jmap -dump:live,formatb,file/data/dump/heap_live.hprof 12345formatb表示二进制格式不加就是文本格式MAT 读不了。文件名里带时间戳是个小习惯同一个进程多次 dump 时不会互相覆盖后面做两份对比也用得上。关键在于live这个开关加了它会先做一次 Full GC好处是 dump 文件小、里面只剩存活对象结论更接近“真实泄漏”坏处是这次 Full GC 在生产上是实打实的长停顿几 GB 堆停个一两秒很常见业务侧能感知到。第二种是jcmdJDK 7 以后都推荐用这个# 推荐方式JDK 7 及以上可用参数比 jmap 更规范 jcmd 12345 GC.heap_dump /data/dump/heap.hprof # 加上 -all 会把不可达对象也写进去等价于 jmap 不带 live jcmd 12345 GC.heap_dump -all /data/dump/heap_full.hprofjcmd的优势是它走的是 JVM 内部的 attach 通道行为和 JVM 版本绑定得更紧不像jmap在部分 JDK 版本上被重写过实现、偶尔出现 dump 失败。JDK 9 之后jmap -dump在某些发行版上会提示要用-F强制实际上不如直接用jcmd。第三种是 OOM 现场自动落盘这也是我在生产环境必配的# 启动参数里加上OOM 发生的瞬间自动 dump -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/ # 可选dump 前先做一次 Full GC减少文件体积 -XX:HeapDumpBeforeFullGCHeapDumpPath指向的目录要提前确认磁盘余量堆多大文件就差不多多大3GB 堆打出来的文件也在 3GB 上下。线上曾经出现过 dump 把数据盘写满、连带日志写不进去的事故血泪经验是给这个目录单独挂一块盘或者加配额。方式是否 STW是否含不可达对象适用场景jmap -dump是是事后补采想看到完整现场jmap -dump:live是且先 Full GC否只关心存活对象能接受一次停顿jcmd GC.heap_dump是是JDK 7 首选OOM 自动 dump发生在 OOM 时是线上必配抓第一现场表格里“是否含不可达对象”这一列最容易被忽略。带了不可达对象的 dump 文件会明显偏大Histogram 里会出现一堆本该被回收的临时对象如果拿它去判断泄漏很容易把垃圾当成嫌疑人。2.2 hprof 不是文本MAT 第一次打开为什么要建索引hprof 是一种带记录头的二进制流。文件开头是一段 header记录版本号、标识符长度4 或 8 字节、时间戳后面跟着一串 record每条 record 有类型标记和长度。和内存分析相关的主要是这几类HEAP_DUMP/HEAP_DUMP_SEGMENT真正的对象图数据包含类加载、实例字段值、数组元素、GC Root 等子记录LOAD_CLASS类加载记录把 class serial number 映射到类名STACK_TRACE/STACK_FRAME线程栈快照HEAP_DUMP_END结束标记MAT 打开文件时做的事是把这些顺序排列的 record 重建成一张可随机访问的对象图。它不能边读边算因为支配树、Retained Size 这些指标都需要全局的引用关系。所以第一次打开会在 hprof 同目录下生成一个heap.hprof.index目录里面是若干二进制索引文件索引文件作用o2hobject id 到内部句柄handle的映射a2s对象地址到对象大小的映射domIn/domOut支配树的入边与出边Retained Size 靠它算idx分段的元信息记录每个 segment 的偏移inbound反向引用索引Path to GC Roots 靠它索引生成的时间和堆大小近似线性一份 3GB 的 dump 在普通 SSD 上大概要两三分钟机械盘能拖到十分钟以上。索引目录本身也不小经验值是 hprof 文件的三分之一到一半。所以 2.2 节要强调的第一件事是hprof 所在目录必须有写权限并且留足空间。把它挂在只读的 NFS 上MAT 会卡在 “Building index” 然后失败报错信息还不一定指向权限问题。索引生成之后后续打开走的是索引速度会快很多。这也解释了为什么有人换了台机器、把 hprof 拷过去但没拷.index目录又要重新等一遍。提示索引文件和 hprof 是绑定关系。hprof 被截断、被重新 dump 覆盖但索引没删MAT 会读到错乱的对象图症状是类名乱码或者IllegalArgumentException。遇到这种情况删掉.index目录重来。2.3 用命令行先把索引和报告跑出来GUI 打开大文件时卡死很多时候不是内存不够而是界面线程在解析过程中被阻塞看起来像挂了。我的做法是先绕开界面用 MAT 自带的 headless 脚本把索引和标准报告一次性跑出来# 进入 MAT 安装目录ParseHeapDump 在根目录下 cd /opt/mat # 第一个参数是 hprof 路径后面跟要生成的分析报告 # api:suspects 泄漏嫌疑报告api:overview 概览api:top_components 大对象排行 ./ParseHeapDump.sh /data/dump/heap.hprof \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview \ org.eclipse.mat.api:top_components执行完会在 hprof 同目录下产出几个 zip 包命名类似heap_Leak_Suspects.zip、heap_System_Overview.zip解压后是 HTML浏览器直接打开就能看。参数说明org.eclipse.mat.api:suspects是报告生成器的 IDID 写错不会报错只是那一步被静默跳过所以跑完要确认 zip 文件确实生成了。这一步的隐含前提是 MAT 自己的堆够大。headless 脚本读取的是同目录下MemoryAnalyzer.ini里的-Xmx配置默认值往往只有 1024m 或 2048m对付几 GB 的 dump 远远不够。修改方法# 编辑 /opt/mat/MemoryAnalyzer.ini找到 -vmargs 段 -vmargs -Xmx8g # 如果是 JDK 8 环境还需显式指定元空间JDK 17 之后不需要 -XX:MaxMetaspaceSize512m-Xmx给多少合适我一般按 dump 文件大小的 1.5 倍来配3GB 的堆文件给 6~8GB。给多了浪费给少了就会出现第 1 章那种 “工具自己 OOM”。注意-Xmx必须写在-vmargs之后写在前面会被 Eclipse 启动器忽略这是个很隐蔽的坑配完可以用jcmd mat_pid VM.flags确认一下实际生效值。3. 四个视图怎么读Histogram、Dominator Tree、Retained Size 与 Leak SuspectsMAT 打开一份 dump 后左侧面板一排功能入口新手最容易犯的错是把 Leak Suspects 当成判决书看到“嫌疑”两个字就开始改代码。这四个视图各自回答的是不同问题混用会把方向带偏。先把每个视图的输入输出和计算口径讲清楚再看怎么串起来用。3.1 Histogram 看“谁多”Dominator Tree 看“谁压着谁”Histogram 是最简单的聚合视图按类分组列出每个类的实例数和 Shallow Heap 总量。它回答的是“哪类对象占的坑位最多”。Class Name | Objects | Shallow Heap java.lang.String | 812,344 | 19,496,256 char[] | 795,102 | 318,040,800 java.util.HashMap$Node | 402,118 | 12,867,776 com.example.cache.LocalEntry | 388,901 | 15,556,040 java.util.concurrent.ConcurrentHashMap$Node | 201,006 | 6,432,192char[]排在前面极其正常String 的底层存储就是它Java 8 之后改成 byte[] 也类似。真正要看的是第三行往后那些“业务类”以及它们和容器类的比例关系。上面这张表里LocalEntry有接近 39 万个实例而 HashMap$Node 是 40 万比例接近 1:1说明这些 Entry 大概率就是被 HashMap 持有的缓存条目。Histogram 的盲区在于它只统计 Shallow Heap也就是对象自身占的字节数不含它引用的子对象。一个ArrayList自身可能只有 24 字节但它引用着一个几百 MB 的数组在 Histogram 里依然排在几百名开外。Dominator Tree 补的正是这个缺口。它按支配关系组织如果从 GC Root 出发到达对象 B 的每一条路径都必须经过 A那么 A 支配 B。树上的一个节点显示的是它“独占”的那部分内存把父节点砍掉子节点就全没了。所以 Dominator Tree 里排行靠前的往往是真正压着大块内存的那几个对象。实际用的时候我的顺序是先看 Leak Suspects 拿方向再进 Histogram 找类的实例数排序怀疑某个类之后右键Merge Shortest Paths to GC Roots或者直接看 Dominator Tree 确认它到底占了多少实际内存。反过来先用 Dominator Tree 也成立但它对新手不友好因为根节点往下第一层经常是一堆java.lang.Thread或者sun.misc.Launcher$AppClassLoader看不出业务含义。3.2 Retained Size 是支配树算出来的不是加法Retained Size 是 MAT 里被误读最多的一个指标。它的定义是把这个对象连同它支配的所有对象全部回收之后能释放的堆大小。注意“支配”两个字它不是简单的“这个对象加上它引用的所有对象”。举个具体例子一个HashMap持有 100 个 Entry每个 Entry 的 value 是个 10MB 的 byte[]。如果这 100 个 byte[] 只被这个 HashMap 引用那么 HashMap 的 Retained Size 大约是 1000MB 加上一些零头。但如果其中 30 个 byte[] 同时被另一个全局缓存引用着那这 30 个就不受这个 HashMap 支配Retained Size 只有 700MB 左右。MAT 算的是后者。这个口径直接影响判断。见过有人看到某个对象 Retained Size 只有几 MB 就放过了实际上它持有着几百个被多处共享的大对象只是那些对象不归它独占单个看都不大合起来才是问题。这种情况要看 Dominator Tree 的子树结构或者用with outgoing references手动展开。指标计算口径适合回答Shallow Heap对象自身字节数不含引用哪个类实例最多Retained Heap该对象支配的所有对象的 Shallow 之和干掉它能释放多少Retained Set该对象支配的对象集合本身具体是哪些对象被压着还有一点Retained Size 默认是懒计算的。刚打开 dump 时Histogram 里那一列可能显示为空或者显示一个问号需要点一下表头的 “Calculate Retained Size” 或者右键菜单里触发一次全量计算几 GB 的堆上这一步要等十几秒到几分钟。很多人没等它算完就开始读数字读到的是 0结论自然错。3.3 Leak Suspects 的判定逻辑与局限Leak Suspects 是 MAT 自动生成的一份报告核心逻辑是找出那些 Retained Size 超过堆总量一定比例默认 1% 左右可以在 Preferences 里调的对象集合然后对每个可疑集合做一次“如果这是泄漏它是从哪进来的”路径分析。它的输出长这样Problem Suspect 1 描述“某某类的 388,901 个实例占用 450MB占总堆 42%”下面跟一条从 GC Root 到该对象的引用链。这条链的价值在于它给出了一个可能的入口比如ThreadPoolExecutor$Worker - Thread - threadLocals - ThreadLocalMap - Entry - value。局限也很明显。第一它只对“大块且集中”的泄漏敏感那些分散在几十个类里、每个只占 0.5% 的缓慢增长它抓不到。第二它的路径分析会包含所有引用类型包括弱引用、软引用所以经常给出一些看着吓人但实际上是正常设计的链路。第三它对“不是泄漏但就是大”的缓存类结构会误报比如一个设计上就允许占 30% 堆的本地缓存每次都被列成头号嫌疑。我的用法是把它当索引而不是结论从 Suspect 列表里挑出和业务相关的那个右键Path to GC Roots重新按强引用算一遍确认链路成立再往下查。3.4 节会讲 OQL 和线程视图怎么补它的短板。3.4 OQL 查询与线程视图OQL 是 MAT 内置的对象查询语言语法接近 SQL可以直接对对象图做条件过滤。它的价值在于把“肉眼在几万行表格里找”变成“一条查询”。-- 查出所有长度超过 10MB 的 byte 数组按大小降序 SELECT a FROM byte[] a WHERE a.length 10485760 -- 查 HashMap 里 table 数组特别大的实例用于定位超大容器 SELECT h FROM java.util.HashMap h WHERE h.table.length 100000 -- 查某个业务类的实例并只看持有超过 1000 条记录的 SELECT c FROM com.example.cache.LocalCache c WHERE c.entries.size() 1000语法说明length是 MAT 给数组类型的内置属性前缀表示这是对象元信息而不是字段h.table直接访问 HashMap 的table字段MAT 走的是反射读取字段值字段名要对得上编译后的实际名字c.entries.size()调用的是方法MAT 支持有限的方法调用只支持无副作用的简单 getter 和集合 size 这类。OQL 的执行是遍历对象图几百万对象的堆上跑一条不带索引条件的查询可能要几十秒条件写得越具体越快。查询结果可以直接右键Show Retained Set或者导出成 CSV。线程视图在Thread Overview里列出 dump 时刻所有线程及其栈帧点进某个线程能看到它栈上的局部变量以及这些变量对应的对象。定位“某个线程里挂着一个巨大集合”这类问题这个视图比 Histogram 直接得多因为泄漏源就在栈上一眼能看到是哪个方法没释放。4. 顺着一条引用链找到根Path to GC Roots 的排除项与实操前面都是找嫌疑人这一章讲怎么定案。Path to GC Roots 是 MAT 里最核心的定位手段但它的默认行为会给出大量噪音路径不设排除项的话一条 ThreadLocal 相关的链能展开出七八条分支看半天不知道哪条是真的。4.1 Path to GC Roots 的四种排除组合右键任意对象Merge Shortest Paths to GC Roots下面有四个子菜单选项排除了什么什么时候用with all references不排除想看完整引用图噪音最多exclude weak references弱引用基本必选排除 WeakHashMap、ThreadLocal keyexclude weak/soft references弱引用 软引用排查真实强引用链的标准姿势exclude all phantom/weak/soft etc.所有非强引用最干净但可能丢掉线索标准操作是选exclude weak/soft references。原因很实际ThreadLocalMap.Entry的 key 是弱引用value 是强引用。如果只选with all referencesMAT 会给出Thread - threadLocals - ThreadLocalMap - Entry - key(弱引用)这条路径看起来线程持有着 Entry实际上 key 早该被回收了真正的问题在 value 那一侧。排除弱引用之后剩下的链路才是“真实存在、无法被 GC 回收”的引用。exclude all phantom/weak/soft etc.用得少一些但在排查 ClassLoader 泄漏时有用。应用热部署场景下老的 ClassLoader 被Finalizer或者PhantomReference队列间接持有这类引用虽然最终会被清理但在 dump 的那一刻确实还挂着需要单独判断。注意排除项只影响路径展示不影响对象图中的实际引用关系。排除之后如果一条路径都找不到说明这个对象其实只被弱引用持有它不构成泄漏这个结论本身就有价值。4.2 ThreadLocal 与线程池的引用链怎么读线程池 ThreadLocal 是 Java 服务里最高频的泄漏组合。典型链路在 MAT 里长这样Thread (线程池里的 Worker 线程) - java.lang.ThreadLocal$ThreadLocalMap threadLocals - java.lang.ThreadLocal$ThreadLocalMap$Entry[] table - java.lang.ThreadLocal$ThreadLocalMap$Entry value - com.example.context.UserContext (持有大对象)读这条链的关键是看最后那个value指向什么以及Entry的 key 是什么。如果 key 是一个已经被回收的 ThreadLocal显示为null或者 MAT 会标注referent null说明这个 Entry 是个“僵尸条目”key 被弱引用回收了value 还牢牢挂在线程的 threadLocals 上直到线程被复用并且调用remove()或者set()触发清理。线程池里的线程生命周期和进程一样长这些 value 就永远活着。常见做法是在finally块里强制remove()// 错误写法线程池复用线程ThreadLocal 不清理会一直挂着 private static final ThreadLocalUserContext CTX new ThreadLocal(); public void handle() { CTX.set(new UserContext(...)); // 大对象塞进去 doBusiness(); // 没有 remove下一次线程复用时旧 value 还在 }// 正确写法无论业务是否抛异常都清理 public void handle() { try { CTX.set(new UserContext(...)); doBusiness(); } finally { CTX.remove(); // 关键一行 } }如果线上不方便改代码另一个观察角度是在 MAT 里直接看ThreadLocalMap$Entry的实例数。正常应该在几十到几百如果出现几万个基本可以确认有地方在反复 set 而不 remove。再看这些 Entry 的value类的分布就能定位到具体是哪个组件。4.3 两份 dump 的 histogram 差分单份 dump 只能看到“当下谁最大”判断不了“什么在增长”。真正的泄漏特征是单调增长所以间隔一段时间打两份 dump 做差分是比单点分析更可靠的方法。MAT 本身没有直接的双文件 diff 功能Compare Basket只能比较当前会话里的两组 Histogram 数据不能跨文件。我的做法是导出 CSV 再用脚本差分# 对比两份 MAT 导出的 histogram CSV找出实例数增长最多的类 # 用法python diff_hist.py heap_t1.csv heap_t2.csv import csv import sys def load(path): 读取 MAT 导出的 Histogram CSV返回 {类名: 实例数} result {} with open(path, newline, encodingutf-8) as f: reader csv.reader(f) next(reader, None) # 跳过表头行 for row in reader: if len(row) 2: continue cls row[0].strip() try: # 第二列是实例数可能带千分位逗号 count int(row[1].replace(,, )) except ValueError: continue result[cls] count return result t1 load(sys.argv[1]) t2 load(sys.argv[2]) # 只保留增长量大于 1000 的类按增量降序输出 diffs [] for cls, c2 in t2.items(): c1 t1.get(cls, 0) delta c2 - c1 if delta 1000: diffs.append((delta, cls, c1, c2)) for delta, cls, c1, c2 in sorted(diffs, reverseTrue)[:30]: print(f{delta:10d} {cls} ({c1} - {c2}))这段脚本做的事很直接读两份 CSV按类名对齐算实例数增量过滤掉小于 1000 的噪音按增量排序输出前 30 行。参数就两个文件路径导出的方式是在 MAT 的 Histogram 视图右上角菜单选Export to CSV两份文件的格式要一致否则列偏移会导致解析出错。增量最大的类不一定就是泄漏源可能是正常的缓存预热或者连接池扩容。判断方法是看这个类的实例数是不是和第二个文件的时间间隔成比例——如果两个 dump 隔了 10 分钟实例数涨了 5 万而且业务量没有对应增长那基本可以锁定。这时候再回到第二份 dump 里对这个类做一次 Path to GC Roots链路就清晰了。5. MAT 排查避坑六个现场与参数级解法这一章是纯踩坑记录。前面几章讲的是“应该怎么做”这一章讲“做的时候会在哪里翻车”。每条都按现象、原因、解决三段写都是我在实际排查里撞过的。5.1 打开与索引阶段三个直接卡死的现场现象一进度条走到 “Building index” 就停住几分钟后弹 OutOfMemoryError。原因MAT 自身的-Xmx不够。索引阶段要把对象图和反向引用索引加载进内存需要的空间和 hprof 大小正相关默认的 1024m 对付 2GB 以上的 dump 必然崩。另一个隐性原因是 dump 里不可达对象太多索引的数据量比存活对象大一圈。解决改MemoryAnalyzer.ini里的-Xmx按 dump 大小的 1.5 倍配。如果机器内存有限退而求其次的办法是先用jmap -dump:live重新采一份小一点的或者用-Dorg.eclipse.mat.parser.maxNeededHeap之类参数限制索引缓存这个参数在不同版本行为不一致稳妥起见还是加内存。改完 ini 记得确认没有第二处-Xmx覆盖。现象二报错Could not read heap dump或者类名显示成乱码Ljava/lang/Str...。原因hprof 文件被截断。常见于 dump 过程中进程被杀、磁盘写满、跨机器传输时 scp 中断。hprof 没有校验和截断的文件在结构上仍然是“看起来合法”的MAT 解析到一半才发现 record 长度对不上。另一种可能是用了非标准的 dump 工具生成的文件格式和 hprof 规范有偏差。解决先看文件大小是不是符合预期和堆配置对比再用jhat或者file命令粗验一下。确认截断就只能重新 dump。传输大文件时用rsync --partial或者带校验的传输方式别用裸 scp。现象三索引跑完了但每次打开都要重新建慢得没法用。原因.index目录写不进去。hprof 放在只读挂载点、或者当前用户对该目录没有写权限MAT 静默失败下次打开继续重建。这个坑特别隐蔽因为 MAT 不会明确提示权限问题。解决chmod或者把 hprof 拷到本地可写目录再分析。另外检查磁盘剩余空间索引目录本身要占 hprof 的三分之一左右空间不够时也会静默失败。5.2 结论阶段三个把方向带偏的误判现象四Histogram 里某个类的 Retained Size 全是 0 或者空白以为这个类不占内存。原因Retained Size 是懒计算的刚打开 dump 时没有触发全量计算表格里那一列是空的。很多人扫一眼就走了错过真正的大头。解决在 Histogram 视图里点表头的Retained Heap列或者右键菜单选Calculate Retained Size等它算完几 GB 的堆要几十秒。算完之后数字才会填上。另外注意Shallow Heap 很小但 Retained 很大的类是常态别被第一列骗了。现象五Path to GC Roots 指向ThreadLocalMap$Entry - key判定为 ThreadLocal 泄漏改完代码没效果。原因没有排除弱引用。Entry的 key 是弱引用MAT 默认路径展示会把它算进去看起来像线程持有 key实际上 key 早就该被回收。真正的问题在value那一侧而 value 的链路被 key 的噪音盖住了。解决重新用exclude weak/soft references跑一遍看剩下的强引用链指向哪里。如果强引用链断了说明确实是弱引用持有不构成泄漏如果还在那才是真的。这个操作重复做几次不丢人我自己也经常第一遍忘了切排除项。现象六OOM 自动 dump 的文件分析不出问题Histogram 里全是本该回收的临时对象。原因HeapDumpOnOutOfMemoryError采的是 OOM 那一刻的完整堆包括所有不可达但还没被 GC 扫到的对象。这些垃圾会稀释信号让真正的泄漏对象排名下降。更麻烦的是OOM 时刻的堆状态可能和稳定运行时有很大差异比如正在做大批量计算。解决分析时在 MAT 的 Preferences 里勾上Keep unreachable objects的反选项默认就是不保留但索引阶段已经写进去了或者先用jmap -dump:live补采一份对比。我的习惯是 OOM 现场那份留档另外在业务低峰期用jcmd GC.heap_dump不带-all再采一份两份对照着看判断哪些是稳态泄漏、哪些是瞬时峰值。6. 大堆进阶批处理固化、报告留档与一次深刻教训当 dump 超过 8GBGUI 基本没法用了翻一页表格卡三秒OQL 跑一条查询要等一分钟。这时候唯一的出路是把分析流程批处理化让 MAT 在后台把该算的都算完人只看结果。我的标准动作是这样一套脚本#!/bin/bash # analyze_dump.sh - 对指定 hprof 做一次完整的无界面分析 # 用法./analyze_dump.sh /data/dump/heap.hprof 8g set -e HPROF$1 HEAP${2:-8g} MAT_HOME/opt/mat # 临时把 MAT 的堆参数改成命令行传入的值避免每次都手改 ini sed -i s/^-Xmx.*/-Xmx${HEAP}/ ${MAT_HOME}/MemoryAnalyzer.ini # 依次生成泄漏嫌疑、概览、大对象排行三份报告 ${MAT_HOME}/ParseHeapDump.sh ${HPROF} \ org.eclipse.mat.api:suspects \ org.eclipse.mat.api:overview \ org.eclipse.mat.api:top_components # 导出 histogram 供后续差分-formatcsv 是导出格式参数 ${MAT_HOME}/ParseHeapDump.sh ${HPROF} \ -Dorg.eclipse.mat.api.parse.runhistogram echo 报告已生成$(dirname ${HPROF}) ls -lh $(dirname ${HPROF})/*.zip脚本里sed那行是偷懒做法生产上更稳妥的是复制一份 ini 或者用环境变量覆盖。ParseHeapDump.sh的第二个参数之后跟的都是报告生成器 ID可以一次传多个MAT 会在同一个会话里依次跑完比开三次进程省时间。最后那个-D参数是给 MAT 传 JVM 系统属性用于指定额外的导出动作具体可用的属性名随版本变化用之前建议先看对应版本的文档或者直接跑ParseHeapDump.sh -help。报告固化之后跟进方式就变成把每次 OOM 或定期巡检的 zip 包按日期归档配合 4.3 节的 CSV 差分脚本形成一条增长曲线。这比每次出问题再临时抓 dump 有效得多因为你能看到“从哪天开始涨”的拐点而不是只知道“现在很大”。还有一点值得单独说堆对比时两份 dump 的 JVM 参数和类路径要一致。曾经为了省事拿一份 JDK 8 的 dump 和一份升级到 JDK 11 之后的 dump 做差分结果HashMap的实现在两个版本里内部结构有差异实例数对比完全失真白白花了一个下午追一个不存在的泄漏。进阶技巧里最省时间的一个是给常用查询存成 OQL 模板。MAT 支持把 OQL 保存成.oql文件放在指定目录下次从下拉框直接选。我常驻的三条是查超过 10MB 的 byte[]、查实例数超过 5 万的业务类、查所有ThreadLocalMap$Entry里 value 非空的条目。这三条覆盖了八成以上的排查入口比每次手写 SQL 快得多。最后说个教训。早期排查一次线上内存告警我拿到 dump 就直接在 Histogram 里按 Shallow Heap 排序看到char[]排第一就断定是字符串拼接问题让开发去改日志拼接逻辑。改完一周后又告警了这次老老实实算了 Retained Size发现真正的大头是一个本地缓存组件里的ConcurrentHashMap$Node它持有的 Entry 里包着大对象Retained 加起来占了 60% 堆而char[]只是这些对象的字符串字段。从那以后我每次打开 dump 都强制走一遍“先算 Retained Size、再看 Dominator Tree、最后才看 Histogram 排序”的流程不再凭第一眼的类名排行下结论。希望这些流程和坑能帮你少走一两个下午。本文还有配套的精品资源点击获取
RELATED

相关推荐

Kafka入门实战:从配置启动到消息生命周期全链路解析

Kafka入门实战:从配置启动到消息生命周期全链路解析

1. 这不是一本“读完就忘”的 Kafka 入门书,而是一套可直接上手的分布式消息系统认知框架你点开这个标题,大概率正站在两个路口之间:一边是刚接触消息队列、被“生产者/消费者/分区/副本”绕得头晕的新人;另一边是已经写过几版 Sp…

📅 2026/10/27 20:38:39
AI短剧全流程自动化生产系统技术解析

AI短剧全流程自动化生产系统技术解析

1. 项目概述:AI短剧全流程自动化生产系统这个开源系统实现了从剧本生成到视频输出的完整短剧生产流水线。作为一名经历过传统影视制作全流程的从业者,我深知剧本创作、分镜设计、拍摄剪辑这个三角循环需要耗费多少人力成本。而眼前这套系统,用…

📅 2026/10/27 20:38:39
2026年AI工具实测:10款降AI率利器提升47%效率

2026年AI工具实测:10款降AI率利器提升47%效率

1. 项目概述2026年AI工具市场将迎来爆发式增长,但随之而来的是信息过载问题。作为一名长期跟踪AI技术发展的从业者,我花了三个月时间实测了市面上87款声称能"降AI率"的工具,最终筛选出10款真正能提升工作效率的利器。这些工具覆盖了…

📅 2026/10/27 20:33:39
MORE NEWS

更多资讯

📰

Pretext文本排版引擎:结构化文档处理新方案

1. Pretext文本排版引擎解析:从技术骨架到实际应用最近在文档处理领域,一个名为Pretext的文本排版引擎开始引起开发者社区的关注。作为一名长期与文档系统打交道的从业者,我最初是被它"排版引擎骨架"的定位所吸引。经过几周的深入研…

📰

UE5 GAS RPG角色属性初始化:ASC时序与GE配置实战

GAS 这套框架最不舒服的地方,从来不是写 GameplayAbility,而是角色刚进场景那一瞬间,属性到底从哪儿来、什么时候生效、客户端跟服务器对不对得上。UE5 GAS RPG 初始化角色的属性这件事,踩过坑的人都懂:服务器上血条明…

📰

Ubuntu命令行高效使用指南:从基础到进阶

1. 为什么需要掌握Ubuntu命令行? 作为Linux发行版中最受欢迎的桌面操作系统,Ubuntu在开发者群体中的占有率长期保持在60%以上。但很多用户安装完系统后,依然习惯性地依赖图形界面操作,这就像买了一辆跑车却只用一档行驶。实际上&a…

📰

营销技能实战指南:文案、活动、数据、沟通全拆解

做营销十年,回头看看"marketing skills"这个词,其实就覆盖了三件事:让人看得进内容、让人信得过方案、让人掏得出钱。什么增长黑客、私域运营、品效合一,本质上都是这三件事在不同平台上的变种。市面上关于营销技巧的书…

📰

营销技能不是堆技巧:一套可拆解、可复盘的实战能力框架

刚入行那阵子,我特别焦虑“marketingskills”这个词。市面上各种课程,从社群裂变到直播带货,从内容营销到品牌定位,标签多到能铺满一堵墙。每个都像武功秘籍,今天学这招明天练那式,结果到真做项目的时候&am…

📰

高并发场景下分布式锁选型与实践指南

1. 项目背景与核心挑战 霸王餐资格发放是外卖平台常见的营销手段,但每逢大促期间,系统面临的并发压力往往呈指数级增长。去年双十一期间,某头部平台就曾因资格超发导致数百万损失。这个项目要解决的正是如何在每秒数万次请求的冲击下&#xf…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬