關於 JVM 垃圾回收 (GC),流傳著許多「經驗之談」:如「定時呼叫 `System.gc()` 能防止 OOM」、「生產環境必須用 JRockit」等。本文將從 HotSpot GC 線程優先級機制、`System.gc()` 的真實行為、JRockit (現 Oracle JRockit/HotSpot 融合) 的並行 GC 架構三個維度,拆解這些認知背後的原理,給出生產環境 JVM 選型與調優的科學建議。
迷思一:自動 GC 線程優先級最低,系統忙時不回收?
結論:錯誤認知。
- GC 線程優先級: HotSpot 中 GC 線程通常運行在 **近最高優先級** (如 `NearCriticalPriority` 或 `CriticalPriority`),**遠高於普通 Java 應用線程 (NormalPriority)**。這是為了確保內存壓力大時 GC 能搶佔 CPU 執行,避免應用線程分配失敗觸發更嚴重的 Full GC 或 OOM。
- 「系統忙時不跑」的真相: 若應用線程佔滿 CPU (計算密集型),GC 線程雖優先級高,但若無安全點 或無可回收對象,確實不會強行搶佔。但這不是優先級問題,而是 **SafePoint 機制** 與 **對象存活率** 決定的。系統越忙 (分配率越高) → 年輕代填滿越快 → **Minor GC 觸發越頻繁**,而非不觸發。
- 元空間/直接內存: 這些區域不受常規堆 GC 管理,若洩漏會導致 `OutOfMemoryError: Metaspace` 或 `Direct buffer memory`,需單獨監控參數 (`-XX:MaxMetaspaceSize`, `-XX:MaxDirectMemorySize`)。
迷思二:定時呼叫 `System.gc()` 兩次效果最好?
結論:極度危險的反模式,嚴禁在生產業務代碼中使用。
`System.gc()` 真實行為
- 語義:
System.gc() 只是向 JVM **「建議」** 執行 Full GC,**不保證** 立即執行、不保證執行、不保證回收特定對象。JVM 規範明確允許忽略此呼叫。
- 實際效果 (HotSpot 預設): 觸發 **Full GC (Stop-The-World)**,掃描整個堆 (含老年代、新生代、元空間)、壓縮老年代 (視 GC 器而定)、處理 Finalizer/Weak/Soft/Phantom 參考佇列。**開銷極大**,單次耗時可達數百毫秒至數秒 (視堆大小)。
- 為何「兩次」? 純屬玄學。首次 Full GC 可能因對象被 Finalizer 佇列引用而未回收,第二次才真正清理。但這說明代碼存在 **Finalizer 濫用或對象引用洩漏**,應修復代碼而非依賴雙重 GC。
生產環境濫用 `System.gc()` 的災難性後果
- 強制 STW: 定時觸發 Full GC 導致**所有業務線程周期性凍結**,直接破壞 SLA (延遲尖峰、超時、熔斷連鎖反應)。
- 破壞分代假設: 強制提升短命對象到老年代,增加老年代佔用,反而誘發更頻繁、更嚴重的 Full GC (惡性循環)。
- 干擾 GC 人體工學: 現代 GC (G1, ZGC, Shenandoah) 依賴運行時數據自適應調整,外部強制 GC 會誤導其決策邏輯。
- RMI 分散式 GC 副作用: 若啟用 RMI (預設每小時觸發一次 `System.gc()`),需關閉或調整間隔 (`-Dsun.rmi.dgc.server.gcInterval=3600000`)。
鐵律: **業務代碼中永不主動呼叫 `System.gc()`。** 僅在以下極罕見場景可考慮:堆外內存釋放 (DirectBuffer 清理)、測試代碼驗證引用釋放、JMX 手動觸發排查問題。若框架/庫 (如 Netty, NIO) 內部呼叫,屬於框架行為,開發者不可控。
JRockit / 現代並行 GC 的真實優勢
文中提到的 JRockit (BEA, 後併入 Oracle) 確實在 2000s 末至 2010s 早期擁有領先的**並行/並發 GC 技術**,但現狀已大變:
歷史地位
- 首創並行 GC: JRockit 的
genpar (分代並行) 和 singlepar (單代並行) 在多核伺服器上大幅超越早期 HotSpot 串行/並行 GC。
- 決定性 GC: 提供
-XpauseTarget 目標停頓時間控制,早期實現「軟實時」特性。
- 任務導向優化: JRockit Mission Control (JMC) / Flight Recorder (JFR) 是當時最強診斷工具套件。
現狀:技術融合與新王者
| JVM/GC |
狀態 |
核心特性 |
適用場景 |
| Oracle JRockit |
⛔ **已終止生命週期** (JDK 6u18 後融合入 HotSpot,JDK 8 後不再獨立發布) |
技術已移植至 HotSpot (Parallel GC, JFR, JMC) |
僅維護極舊系統 |
| HotSpot Parallel GC |
✅ 成熟穩定 (JDK 8 預設) |
多線程並行回收,吞吐量優先,STW 較長 |
批次處理、科學計算、吞吐量敏感 |
| HotSpot G1 GC |
✅ JDK 9+ 預設 |
區域化、增量整理、可預測停頓、混合 GC |
通用首選,堆 4GB-32GB,延遲敏感 |
| ZGC / Shenandoah |
✅ JDK 11+ (生產就緒) |
超低延遲 (<10ms)、並發整理、壓縮、支援 TB 級堆 |
大堆 ( >32GB )、極致低延遲、實時系統 |
| Azul Zing (ZST) |
💰 商業授權 |
C4 (Continuously Concurrent Compacting Collector)、無停頓 GC、需專用 Linux |
金融高頻交易、超大堆、零停頓剛需 |
生產環境選型與調優決策樹 (2024+ 版)
- JDK 版本: **強制 JDK 17 LTS 或 JDK 21 LTS** (長期支援、效能優化、安全更新)。
- 預設起步: **G1 GC (JDK 9+ 預設)**,無需額外參數即可工作良好。設定目標停頓:
-XX:MaxGCPauseMillis=100 (視業務 SLA)。
- 堆大小設定:
-Xms -Xmx 設為相同值 (避免動態縮容震盪),佔容器記憶體 70-80% (留給 Metaspace, CodeCache, ThreadStack, DirectMemory, OS)。
- 大堆/低延遲: 堆 > 32GB 或 P99 延遲 < 50ms → **ZGC** (`-XX:+UseZGC -XX:+ZGenerational` JDK 21+ 分代 ZGC)。
- 啟用統一日誌:
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m (JDK 9+ 統一格式,必開)。
- 禁用顯式 GC:
-XX:+DisableExplicitGC (防止 RMI/庫/代碼誤觸發 Full GC) + -Dsun.rmi.dgc.server.gcInterval=3600000 (RMI GC 間隔 1 小時)。
- 監控告警: Prometheus + JMX Exporter 抓取 `jvm_gc_pause_seconds`、`jvm_memory_used_bytes`,Grafana 儀表板 + Alertmanager 告警。
總結
JVM GC 不是黑魔法,而是**工程權衡的結晶**。拋棄「手動 `System.gc()` 」、「JRockit 神話」等過時經驗,擁抱 **現代分代/並發 GC (G1/ZGC)、統一日誌、標準化監控、禁用顯式 GC** 的科學體系。記住:**最好的 GC 調優,是寫出「朝生夕死」對象少、大對象少、引用清晰的代碼,讓 GC 自動完成它最擅長的工作。**