Java 堆是所有執行緒共享的記憶體區域,存放幾乎所有物件實例。垃圾回收 (GC) 自動回收不再被引用的物件,**釋放開發者手動管理記憶體的負擔**,但也帶來**停頓 (Pause)、CPU 佔用、記憶體碎片、調優複雜度**等挑戰。本文系統講解 GC 核心算法、七大收集器演進、JMM 記憶體區域、生產級調優方法論、常見異常診斷。
| 算法 | 原理 | 優點 | 缺點 | 應用 |
|---|---|---|---|---|
| 引用計數法 | 物件頭計數器,引用+1,釋放-1,歸零即可回收 | 即時性、無停頓、實時系統友好 | **無法解決循環引用**、計數器開銷大、多執行緒同步複雜 | Python/CPython、COM、早期 JVM 實驗 (未採用) |
| 可達性分析 (根搜尋) | 從 GC Roots (棧帧局部變數、JNI 靜態欄位、同步鎖、類載入器、啟動類) 向下搜尋,可達 = 存活 | **解決循環引用**、理論完備、JVM 標準 | 需掃描整個物件圖、Stop-The-World (STW) 掃描根節點 | **所有現代 JVM (HotSpot, J9, Zing, Graal)** |
GC Roots 類型:虛擬機棧引用、方法區靜態屬性/常量引用、本地方法棧 JNI 引用、同步鎖持有物件、系統類載入器載入的類、JVM 內部物件 (異常/類/異常處理)、Thread 物件、Class 物件等。
| 算法 | 核心思想 | 記憶體佈局 | 優點 | 缺點 | 演進 |
|---|---|---|---|---|---|
| 標記-清除 (Mark-Sweep) | 標記存活 → 清除未標記 | 不連續 | 實現簡單、無需移動物件 | **碎片化嚴重**、分配效率低 (自由鏈表)、兩次掃描 | 基礎算法,CMS 初期 |
| 標記-整理/壓縮 (Mark-Compact) | 標記存活 → 移動存活物件至一端 → 更新引用 | 連續 | **無碎片**、指針碰撞分配極快 | 移動物件開銷大、需更新所有引用 (STW)、雙倍掃描 | 老年代收集器主流 (Parallel Old, Serial Old, G1 混合回收) |
| 複製 (Copying) | 劃分 From/To 區,存活物件複製至 To 區,角色互換 | 連續 (半區浪費) | **無碎片**、分配極快 (指針碰撞)、單次掃描 | **記憶體利用率 50%**、大物件複製開銷大、需讀/寫屏障 | 新生代主流 (Serial, Parallel Scavenge, G1 Young, ZGC/Shenandoah) |
分代假說:多數物件朝生夕死 (新生代用複製)、少數物件長期存活 (老年代用標記-整理)。**現代收集器皆採用分代/分區混合策略。**
| 收集器 | 年代 | 算法 | 併發/並行 | 特色 | 適用場景 | 啟動參數 |
|---|---|---|---|---|---|---|
| Serial | 新/老 | 複製/標記-整理 | 單執行緒 (STW) | 簡單、佔用極小、Client 模式預設 | 單核、嵌入式、小記憶體 (<100MB) | -XX:+UseSerialGC |
| Parallel Scavenge / Parallel Old | 新/老 | 複製/標記-整理 | **多執行緒並行 (STW)** | **吞吐量優先**、自適應調節 (GC Ergonomics)、JDK 8 Server 預設 | 批次處理、科學計算、無低延遲要求、多核 | -XX:+UseParallelGC (JDK 8 預設) |
| CMS (Concurrent Mark Sweep) | 老 | 標記-清除 | **多執行緒併發 (大部分階段)** | **低延遲目標**、碎片化、浮動垃圾、CPU 敏感、JDK 9 棄用, 14 移除 | 舊系統低延遲需求 (已遷移 G1/ZGC) | -XX:+UseConcMarkSweepGC |
| G1 (Garbage First) | 全堆 (分區) | 標記-整理 + 複製 (混合) | 多執行緒並行 + 併發標記 | **可預測停頓**、**分區管理**、混合回收、大物件區、JDK 9+ 預設 | **通用首選**、大堆 (4G-32G)、延遲敏感、JDK 17+ 生產主流 | -XX:+UseG1GC (JDK 9+ 預設) |
| ZGC (Z Garbage Collector) | 全堆 (分區) | 著色指標 + 讀屏障 + 併發整理 | **幾乎全併發 (亞毫秒停頓)** | **超低延遲**、支援 TB 級堆、JDK 11 實驗, 15 生產、無分代 (JDK 21 引入分代) | 極低延遲 ( <1ms )、大堆 ( >32G )、高併發 | -XX:+UseZGC |
| Shenandoah | 全堆 (分區) | 併發複製 + 寫屏障 + Brooks 指針 | **幾乎全併發 (亞毫秒停頓)** | **超低延遲**、Red Hat 主導、OpenJDK 內建、JDK 12 生產、支援分代 (JDK 20+) | 極低延遲、大堆、Red Hat 生態 | -XX:+UseShenandoahGC |
| Generational ZGC | 分代 | ZGC 新生代複製 + 老年代併發整理 | 全併發 | **JDK 21 GA**:結合分代假說優化分配速率、減少記憶體佔用 | **未來主流**、全場景 (延遲 + 吞吐 + 記憶體) | -XX:+UseZGC -XX:+ZGenerational |
+-------------------------+ <- Heap (Xmx/Xms) | Young Generation | <- Eden + Survivor0 (S0) + Survivor1 (S1) (比例 -XX:SurvivorRatio=8) | (Minor GC / Young GC) | 新物件優先分配 Eden -> 滿觸發 YGC -> 存活進 Survivor -> 年齡增長 -> 晋升老年代 +-------------------------+ | Old Generation | <- Tenured (Major GC / Full GC) | (Major GC / Full GC) | 大物件 (-XX:PretenureSizeThreshold) 直接進老年代、長期存活物件 +-------------------------+ | Metaspace (方法區) | <- 類元數據、常量池、靜態變數、JIT 代碼緩存 (Native Memory, -XX:MaxMetaspaceSize) +-------------------------+ | Code Cache | <- JIT 編譯代碼 (Native, -XX:ReservedCodeCacheSize) +-------------------------+ | Thread Stacks | <- 每執行緒私有 (-Xss), 棧幀 (局部變數表/操作數棧/動態連接/返回地址) +-------------------------+ | Direct Memory (NIO) | <- ByteBuffer.allocateDirect, Unsafe, Netty (Native, -XX:MaxDirectMemorySize) +-------------------------+
TLAB (Thread Local Allocation Buffer):Eden 劃分給各執行緒專屬緩衝區,指針碰撞分配 **無鎖、極快**、減少爭用。-XX:+UseTLAB (預設開啟)。
-Xlog:gc*:file=gc.log:time,uptime,level,tags (JDK 9+ 統一日誌) 收集 GC 日誌。jvm/gc), JVisualVM / JMC。-Xms -Xmx (建議設相等避免震盪)、新老比 -XX:NewRatio、Survivor 比 -XX:SurvivorRatio。-XX:MaxTenuringThreshold (預設 15, G1 動態)。-XX:+UseG1GC -XX:MaxGCPauseMillis=50);極低延遲選 ZGC/Shenandoah (-XX:+UseZGC);吞吐優先選 Parallel (-XX:+UseParallelGC)。-XX:G1HeapRegionSize (1-32MB, 2 的冪)、-XX:InitiatingHeapOccupancyPercent (觸發併發標記堆佔用%, 預設 45)、-XX:G1MixedGCCountTarget (混合 GC 目標次數)。System.gc():-XX:+DisableExplicitGC (或 RMI GC 間隔 -Dsun.rmi.dgc.client.gcInterval=3600000)。| 異常 | 原因 | 診斷工具/命令 | 解決方向 |
|---|---|---|---|
java.lang.OutOfMemoryError: Java heap space | 堆耗盡:記憶體洩漏 / 堆太小 / 分配速率過高 | jmap -histo:live pid, jmap -dump:live,format=b,file=heap.hprof pid, MAT/Eclipse Memory Analyzer 分析支配樹、GC Roots 路徑 | 修復洩漏 (靜態集合/緩存/監聽器/ThreadLocal)、增大堆、優化分配 |
java.lang.OutOfMemoryError: Metaspace | 類元數據區耗盡:類載入過多 / 類載入器洩漏 / 動態代理/生成類過多 | jcmd pid GC.class_histogram, jcmd pid VM.metaspace, Arthas classloader 統計 | 增大 MaxMetaspaceSize、修復類載入器洩漏 (熱部署/OSGi/插件)、限制動態生成類數量 |
java.lang.OutOfMemoryError: Direct buffer memory | 直接記憶體耗盡:NIO ByteBuffer/Netty/堆外緩存未釋放 | -XX:+UnlockDiagnosticVMOptions -XX:+PrintDirectMemoryStatistics, Arthas vmtool 分析 DirectBuffer | 增大 MaxDirectMemorySize、使用 Cleaner/ReferenceQueue 釋放、避免頻繁 allocateDirect |
java.lang.StackOverflowError | 棧幀過深:遞歸無終止 / 循環調用 / 大量局部變數 | 異常堆疊追蹤、-Xss 增大 (預設 1MB)、Arthas stack | 改迭代/尾遞歸優化、檢查循環依賴、減少棧幀大小 |
# 1. 統一 GC 日誌 (生產必開,性能影響極小)
-Xlog:gc+heap+age+promotion+phases+ergo*:file=/var/log/app/gc-%p-%t.log:time,uptime,level,tags:filecount=10,filesize=100M
# 2. 容器環境感知 (JDK 10+)
-XX:+UseContainerSupport -XX:InitialRAMPercentage=60 -XX:MaxRAMPercentage=80
# 3. 生產推薦基線 (JDK 17/21)
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+UseStringDeduplication
-XX:+ParallelRefProcEnabled -XX:+UnlockExperimentalVMOptions -XX:+UseTLAB
-XX:-ResizePLAB -XX:+DisableExplicitGC
GC 不是黑盒,而是**可觀測、可分析、可調優**的工程系統。理解**分代假說、三大算法、七大收集器演進、記憶體區域劃分、分配/晋升/回收流程**,掌握**科學四步調優法、統一日誌、GCeasy/Arthas/MAT 診斷工具鏈**,即可從容應對生產環境記憶體疑難雜症。**最好的 GC 調優是寫出低分配率、短生命週期、無洩漏的代碼** — 這才是根治之道。