
本文檔手把手教你用 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 使用量設置