本文档手把手教你用 JDK 17 生成真实的、MAT 可分析的堆快照文件.hprof并在 MAT 中完成一次完整的内存泄漏分析练习。目录一、前置准备二、编写模拟内存泄漏的 Java 程序三、编译并运行四、生成堆快照三种方式五、用 MAT 打开并分析六、MAT 练习路线图七、常见问题排查八、一键脚本一、前置准备1.1 确认 JDK 版本java-version预期输出JDK 17 即可21 也行openjdk version 17.0.12 2024-07-16 OpenJDK Runtime Environment (build 17.0.127) OpenJDK 64-Bit Server VM (build 17.0.127, mixed mode)⚠️ 不需要 JDK 17 特别版任何 JDK 11 都可以关键是有jmap和jcmd工具。1.2 下载 MAT参照之前的文档下载与你 JDK 匹配的 MAT 版本JDK 版本推荐 MAT 版本下载地址JDK 17MAT 1.14.0https://download.eclipse.org/mat/1.14.0/rcp/JDK 21MAT 1.15.0 / 1.16.0https://www.eclipse.org/mat/downloads.php1.3 创建项目目录mkdir-p~/mat-practicecd~/mat-practice二、编写模拟内存泄漏的 Java 程序2.1 完整源码在项目目录中创建LeakDemo.javaimportjava.util.*;importjava.util.concurrent.*;/** * 模拟内存泄漏的练习程序 * * 包含三种经典泄漏场景 * 1. 静态缓存 Map 无淘汰机制最常见 * 2. ThreadLocal 未清理 * 3. 大对象byte[]持续分配 * * 运行方式 * javac LeakDemo.java * java -Xms512m -Xmx512m -XX:HeapDumpOnOutOfMemoryError \ * -XX:HeapDumpPath./leak.hprof LeakDemo */publicclassLeakDemo{// 场景1静态缓存泄漏 // Spring 单例 Bean 中的 static Map只 put 不 removeprivatestaticfinalMapLong,byte[]productCachenewConcurrentHashMap();// 场景2ThreadLocal 泄漏 // 每个线程持有一个不断增长的 List线程不退出 → 内存泄漏privatestaticfinalThreadLocalListStringthreadLocalDataThreadLocal.withInitial(ArrayList::new);// 场景3大对象列表 // ArrayList 不断 add 1MB 的 byte[]永不清理privatestaticfinalListbyte[]tempBuffersnewArrayList();// 控制程序运行privatestaticvolatilebooleanrunningtrue;publicstaticvoidmain(String[]args)throwsException{longpidProcessHandle.current().pid();System.out.println();System.out.println( LeakDemo - 内存泄漏模拟程序);System.out.println( PID: pid);System.out.println();System.out.println();System.out.println(手动生成堆快照命令);System.out.println( jmap -dump:formatb,fileleak.hprof pid);System.out.println( 或);System.out.println( jcmd pid GC.heap_dump leak.hprof);System.out.println();System.out.println(等待 OOM 自动 dump或手动执行上面的命令...);System.out.println();// 启动监控线程startMonitor();longcounter0;while(running){// --- 场景1缓存泄漏 ---// 每个对象 1MB不停塞入 MapproductCache.put(counter,newbyte[1024*1024]);// --- 场景2ThreadLocal 泄漏 ---// 每个线程的 List 不断增长threadLocalData.get().add(leak-data-counter);// --- 场景3临时缓冲区虽然叫临时但永不清理---if(counter%100){tempBuffers.add(newbyte[1024*1024]);// 每10次加一个1MB}counter;// 打印进度if(counter%500){System.out.printf([%6d] cache%d, threadLocal%d, buffers%d%n,counter,productCache.size(),threadLocalData.get().size(),tempBuffers.size());}// 控制速度避免瞬间 OOM让你有时间手动 dumpTimeUnit.MILLISECONDS.sleep(50);}}/** * 监控线程每 5 秒打印堆内存使用情况 */privatestaticvoidstartMonitor(){ThreadtnewThread(()-{RuntimertRuntime.getRuntime();while(running){longused(rt.totalMemory()-rt.freeMemory())/1024/1024;longmaxrt.maxMemory()/1024/1024;doublepct(double)used/max*100;System.out.printf([Monitor] 堆: %dMB/%dMB (%.0f%%)%s%n,used,max,pct,pct85? ⚠️ 即将OOM!:);try{Thread.sleep(5000);}catch(InterruptedExceptionignored){}}},monitor);t.setDaemon(true);t.start();}}2.2 代码说明场景代码位置模拟的真实问题静态缓存泄漏productCache电商商品缓存无淘汰Map 无限增长ThreadLocal 泄漏threadLocalDataWeb 容器线程池中的 ThreadLocal 未 remove大对象堆积tempBuffers集合只 add 不 clearbyte[] 占满堆三、编译并运行3.1 编译cd~/mat-practice javac LeakDemo.java编译成功后会生成LeakDemo.class文件。3.2 运行带 OOM 自动 dump 参数java-Xms512m-Xmx512m\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath./leak.hprof\LeakDemo或者在pom.xml文件添加配置buildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationjvmArguments-Xms256m-Xmx256m-XX:MetaspaceSize128m-XX:MaxMetaspaceSize256m-XX:UseG1GC-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath./heapdump.hprof-Dfile.encodingUTF-8/jvmArguments/configuration/plugin/plugins/build参数解释参数作用-Xms512m初始堆大小 512MB-Xmx512m最大堆大小 512MB故意设小快速触发 OOM-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动生成堆快照-XX:HeapDumpPath./leak.hprof快照保存路径3.3 预期运行效果 LeakDemo - 内存泄漏模拟程序 PID: 12345 手动生成堆快照命令 jmap -dump:formatb,fileleak.hprof 12345 或 jcmd 12345 GC.heap_dump leak.hprof 等待 OOM 自动 dump或手动执行上面的命令... [Monitor] 堆: 45MB/512MB (8%) [ 50] cache50, threadLocal50, buffers5 [ 100] cache100, threadLocal100, buffers10 [Monitor] 堆: 158MB/512MB (30%) [ 150] cache150, threadLocal150, buffers15 ... [Monitor] 堆: 450MB/512MB (87%) ⚠️ 即将OOM! [ 480] cache480, threadLocal480, buffers48 [Monitor] 堆: 495MB/512MB (96%) [Monitor] 堆: 508MB/512MB (99%) ⚠️ 即将OOM! java.lang.OutOfMemoryError: Java heap space Dumping heap to ./leak.hprof ... Heap dump file created [268435456 bytes in 2.345 secs]leak.hprof文件已自动生成这就是一个真实的、MAT 可分析的堆快照。四、生成堆快照三种方式方式一OOM 自动生成最简单就是上面 3.2 的方式等程序自然 OOM 即可。适合练习和演示。方式二jmap 手动生成最常用 ⭐程序运行时另开一个cmd终端执行# 1. 查找 PIDjps-l# 输出示例12345 LeakDemo# 2. 生成堆快照jmap-dump:formatb,fileleak.hprofPID# 输出Heap dump file created⚠️关键参数说明formatb→ 生成二进制格式MAT 可识别不要加live参数jmap -dump:live,...会先触发 Full GC把所有泄漏对象清掉MAT 就分析不出问题了方式三jcmd 生成JDK 7 推荐# 查找 PIDjcmd# 生成堆快照jcmd12345GC.heap_dump leak.hprofjcmd是 Oracle 官方推荐的替代jmap的新工具功能更全。三种方式对比方式触发条件优点缺点OOM 自动堆满了零操作自动生成服务已崩溃jmap随时手动灵活不影响服务会触发 STW短暂停顿jcmd随时手动官方推荐功能多同 jmap五、用 MAT 打开并分析5.1 打开堆快照启动 MATFile→Open Heap Dump→ 选择leak.hprof选择Leak Suspects Report点击Finish5.2 预期看到的分析结果MAT 会自动生成报告你应该能看到类似以下内容Leak Suspects Report:Dominator Tree支配树Window→Open Dominator Tree按Retained Heap排序你会看到Path To GC Roots关键操作在 Dominator Tree 中右键ConcurrentHashMap选择Path To GC Roots→exclude weak/soft references你会看到引用链③ Histogram直方图Window→Histogram按Objects排序Class Name | Objects | Shallow Heap ----------------------------|---------|------------- byte[] | 528 | 480 MB ← 异常多的 byte[] java.lang.String | 480 | 38 MB java.util.concurrent.ConcurrentHashMap$Node | 480 | 30 MB④ Thread OverviewWindow→Thread Overview查看每个线程的内存占用找到持有大量 ThreadLocal 数据的线程。六、MAT 练习路线图打开堆快照后按以下顺序练习由浅入深第 1 步看自动报告Leak Suspects标签页看 Problem Suspect 摘要点击Details查看详情理解 MAT 的自动分析逻辑第 2 步Histogram 分析Window→Histogram按对象数量排序 → 找异常多的类按 Shallow Heap 排序 → 找大对象右键类 →List objects→with incoming references第 3 步Dominator TreeWindow→Open Dominator Tree按 Retained Heap 排序找到占用内存最大的对象理解 Shallow vs Retained 的区别第 4 步GC Roots 引用链核心技能右键对象 →Path To GC Roots→exclude weak references看清谁在引用这个对象理解为什么 GC 无法回收定位到具体的代码变量名第 5 步OQL 查询Window→OQL-- 查找所有 byte[] 且大小超过 1MB 的SELECT*FROMbyte[]WHERElength1048576-- 查找 String 对象SELECT*FROMjava.lang.String-- 查找 ArrayList 实例SELECT*FROMjava.util.ArrayListWHERElength100第 6 步对比两次快照进阶程序运行 30 秒时 dump 一次 →dump1.hprof程序运行 60 秒时 dump 一次 →dump2.hprofMAT 中打开dump2.hprof→Compare Basket→Add再打开dump1.hprof→Compare Basket→Add点击Compare→ 查看对象增长趋势七、常见问题排查7.1 MAT 打开报错 “Unknown HPROF Version”原因文件不是标准 HPROF 格式可能是文本文件或伪造的。排查# 检查文件头前 4 个字节应该是 JAVAxxd leak.hprof|head-1# 正确输出00000000: 4a41 5641 2050 524f 4649 4c45 2031 2e30# (JAVA PROFILE 1.0)# 检查文件大小ls-lhleak.hprof# 应该 100MB512MB 堆大概产生 200~400MB 的 hprof解决用jmap -dump:formatb,...或jcmd GC.heap_dump重新生成。7.2 MAT 显示 “No leak suspect was found”原因生成快照时加了live参数触发了 Full GC泄漏对象被清掉了。错误命令jmap -dump:live,formatb,fileleak.hprofpid❌正确命令jmap-dump:formatb,fileleak.hprofpid✅7.3 MAT 打开时报 OOM原因MAT 自身内存不足无法解析大文件。解决编辑MemoryAnalyzer.ini-vmargs -Xms2g -Xmx4g ← 调大这个建议设为 hprof 文件大小的 1.5~2 倍 -XX:UseG1GC7.4 生成的 hprof 文件太小只有几 MB原因程序还没来得及分配大量对象就退出了或者堆设太大、还没填满。解决减小堆-Xms256m -Xmx256m更快 OOM加快速度去掉Thread.sleep(50)确保程序真的在运行看控制台输出7.5 jmap 执行卡住或报 “Unable to attach”原因PID 不对或权限不足或容器环境限制。排查# 确认 PIDjps-l# 确认是同一个用户ps-ppid-ouser,pid,cmd# 容器环境用 jcmdjcmdpidGC.heap_dump leak.hprof八、一键脚本8.1 Linux/Mac 一键脚本run.sh#!/bin/bashset-eechoecho MAT 堆快照练习 - 一键运行echoecho# 编译echo[1/3] 编译...javac LeakDemo.javaecho✅ 编译成功echo# 创建输出目录mkdir-pdumps# 运行后台echo[2/3] 启动 LeakDemo后台运行...java\-Xms512m-Xmx512m\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath./dumps/leak_auto.hprof\-XX:PrintGCDetails\-Xloggc:./dumps/gc.log\LeakDemoPID$!echo✅ 进程已启动PID$PIDecho# 等待 30 秒让程序分配一些内存echo[3/3] 等待 30 秒后手动生成堆快照...sleep30# 手动 dump不带 live保留泄漏对象echo 正在生成堆快照...jmap-dump:formatb,file./dumps/leak_manual.hprof$PIDecho✅ 堆快照已保存到 ./dumps/leak_manual.hprofecho# 打印文件信息echo 生成的文件ls-lhdumps/echoecho 用 MAT 打开 ./dumps/leak_manual.hprof 开始练习echo# 等待用户按回车后杀掉进程read-p按回车键停止 LeakDemo...kill$PID2/dev/nullecho✅ 进程已停止8.2 Windows 一键脚本run.batecho off echo echo MAT 堆快照练习 - 一键运行 echo echo. echo [1/3] 编译... javac LeakDemo.java if errorlevel 1 ( echo ❌ 编译失败请检查 JDK 是否安装 pause exit /b 1 ) echo ✅ 编译成功 echo. mkdir dumps 2nul echo [2/3] 启动 LeakDemo后台运行... start /b java -Xms512m -Xmx512m -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath./dumps/leak_auto.hprof LeakDemo echo ✅ 进程已启动 echo. echo [3/3] 等待 30 秒... timeout /t 30 /nobreak nul echo 查找 PID 并生成堆快照... for /f tokens1 %%i in (jps -l ^| findstr LeakDemo) do ( echo PID: %%i jmap -dump:formatb,file./dumps/leak_manual.hprof %%i ) echo ✅ 堆快照已保存到 .\dumps\leak_manual.hprof echo. echo 生成的文件 dir dumps echo. echo 用 MAT 打开 .\dumps\leak_manual.hprof 开始练习 echo. pause九、总结核心要点堆快照必须从真实运行的 JVM 中导出用jmap/jcmd/-XX:HeapDumpOnOutOfMemoryError不要加live参数否则 Full GC 会把泄漏对象清掉formatb必须有确保是二进制格式MAT 的-Xmx要够大建议设为 hprof 文件大小的 1.5~2 倍分析三板斧Histogram → Dominator Tree → Path To GC Roots完整命令速查# 编译javac LeakDemo.java# 运行OOM 自动 dumpjava-Xms512m-Xmx512m\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath./leak.hprof\LeakDemo# 查找 PIDjps-l# 手动 dump推荐jmap-dump:formatb,fileleak.hprofpid# 或 jcmdjcmdpidGC.heap_dump leak.hprof# 验证文件格式xxd leak.hprof|head-1# 应看到4a41 5641 2050 524f 4649 4c45JAVA PROFILE练习建议先用小堆-Xmx256m快速触发 OOM 自动生成快照再用jmap手动在不同时机生成多份快照做对比分析。掌握这套流程后线上遇到真实 OOM 你就能从容应对。附录JVM 参数速查表参数作用推荐值-Xms初始堆大小与-Xmx一致-Xmx最大堆大小根据物理内存设置-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动 dump所有环境必开-XX:HeapDumpPathdump 文件路径确保目录存在且有写权限-XX:PrintGCDetails打印 GC 详情排查必备-XloggcGC 日志路径/data/logs/gc.log-XX:DisableExplicitGC禁止 System.gc()生产环境建议开启-XX:MaxDirectMemorySize限制堆外内存根据 NIO 使用量设置