核心悖論:為何 GC 能提升分配速度?
C++ 堆分配需搜尋空閒塊,易產生碎片;Java 堆採用「指針碰撞」——新物件總在堆頂分配,指針單純前移,速度媲美棧分配。GC 的壓縮整理動作將碎片整理、指針重置,維持分配器的高效率。
主流 GC 演算法演進
| 演算法 | 原理 | 優點 | 缺點 |
| 引用計數 | 物件記錄被引用次數,歸零即回收 | 即時性、增量式 | 循環引用無法處理、計數開銷大 |
| 標記-清除 | 從 GC Roots 遍歷標記存活,掃描清理未標記 | 解決循環引用、無需移動物件 | 產生記憶體碎片、分配需空閒列表 |
| 複製 | 將堆分為 From/To 兩區,存活物件複製到 To 區 | 無碎片、分配極快(指針碰撞) | 記憶體利用率 50%、大物件複製開銷大 |
| 標記-整理 | 標記後將存活物件向一端移動,更新引用 | 無碎片、記憶體利用率高 | 移動物件需更新引用、停頓較長 |
| 分代收集 | 依物件存活率分代 (新生代/老年代),不同代用不同演算法 | 針對性優化、高吞吐/低延遲平衡 | 實作複雜、需調優參數 |
HotSpot JVM 自適應 GC 策略
- 新生代:採用複製演算法 (Eden + 2 Survivor),Minor GC 頻繁但快
- 老年代:標記-整理或標記-清除 (CMS/G1),Major GC 較重
- 自適應切換:JVM 監控 GC 效率,動態在「停止-複製」與「標記-清除」間切換
- 大物件直接進老年代:避免 Survivor 區來回複製
- 分代計數器:物件熬過閾值 (預設 15) 晉升老年代
關鍵執行期優化
類載入與 JIT 編譯
- 類首次使用時載入,字節碼進入方法區
- JIT 編譯器識別熱點方法編譯為機器碼
- 分層編譯:C1 (Client) 快速編譯 + C2 (Server) 深度優化
- 解釋執行與編譯執行無縫切換,避免全量編譯啟動慢、佔記憶體
安全點與停頓控制
- GC 發生在安全點
- -XX:+UseBiasedLocking、-XX:+UseCompressedOops 減少掃描開銷
- G1/ZGC/Shenandoah 等低延遲收集器將停頓控制在毫秒級
常見 GC 參數調優
| 參數 | 作用 |
| -Xms / -Xmx | 初始/最大堆大小 |
| -Xmn | 新生代大小 |
| -XX:SurvivorRatio | Eden:Survivor 比例 |
| -XX:MaxTenuringThreshold | 晉升老年代年齡閾值 |
| -XX:+UseConcMarkSweepGC | 啟用 CMS (老年代並發) |
| -XX:+UseG1GC | 啟用 G1 (區域化、可預測停頓) |
| -XX:+PrintGCDetails | 詳細 GC 日誌 |
記憶體洩漏診斷
- JNI 本地記憶體未釋放
- 靜態集合持有長生命週期引用
- ThreadLocal 未清理
- 最終化器佇列堆積
工具鏈:jmap -histo, jhat, VisualVM, MAT (Eclipse Memory Analyzer), Java Flight Recorder
總結
Java GC 從理論上的「標記-清除/複製」演進為工程化的「分代自適應」系統,配合 JIT 編譯、壓縮指針、偏向鎖等協同優化,實現了「分配極快、回收高效、停頓可控」的記憶體管理目標。理解其核心機制,是進行 JVM 效能調優與疑難雜症診斷的基石。
- 上一篇文章: JDK 1.6 內建 jrunscript:命令列 JavaScript 引擎實戰
- 下一篇文章: Java 效能優化實戰指南:從瓶頸定位到代碼層面調優
Copyright ©
程式碼之家 All Rights Reserved