1. 垃圾回收器(GC)入門
如果你曾經用 C 或 C++ 撰寫過程式,應該對使用 free() 或 delete 手動釋放記憶體很熟悉。在 Java 中就簡單多了:你用 new 建立物件,無需手動刪除——有一位專門的「清潔工」稱為垃圾回收器(Garbage Collector,GC)會處理這件事。
GC 是 JVM 的一部分,會自動釋放那些已經沒有任何參考指向的物件所佔用的記憶體。有了它,Java 開發者就不必擔心忘了清理記憶體(導致洩漏),或反過來不小心刪除仍然需要的物件(導致當機)。
不過,就像任何清潔工一樣,GC 並非完美:有時可能在最不合時宜的時刻介入,來一場「大掃除」(Stop-the-World),或是運作速度不如預期。因此,JVM 提供了多種不同實作的垃圾回收器——而選對了,會顯著影響應用程式的效能。
主要類型的垃圾回收器
Serial GC
- Serial GC —— 最簡單且最早期的垃圾回收器。
- 在單一執行緒中運作。
- 在回收期間會停止其他所有執行緒(Stop-the-World)。
- 適合沒有活躍多執行緒的小型應用。
- 以旗標啟用:-XX:+UseSerialGC
Parallel GC
- Parallel GC(亦稱「Throughput Collector」)。
- 使用多個執行緒進行回收。
- 以最大吞吐量為目標。
- 仍然會產生 Stop-the-World 停頓,但通常比 Serial 更快。
- 適合對少量停頓不敏感的伺服器端應用。
- 以旗標啟用:-XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS —— 已過時但曾流行已久的 GC,目標是最小化停頓。
- 部分與應用並行運作,降低停止時間。
- 設定較為複雜,且有額外開銷。
- 自 Java 9 起標記為 deprecated。
- 以旗標啟用:-XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC —— 現代的預設回收器(自 Java 9 起)。
- 在最小化停頓與整體效能間取得平衡。
- 將堆切分為許多小型區域(區域模型)。
- 可選擇性地按區域收集垃圾,而非處理整個堆。
- 允許設定目標最大停頓,例如 -XX:MaxGCPauseMillis=200。
- 啟用旗標:-XX:+UseG1GC(通常不必指定,因 G1 為預設)。
ZGC 與 Shenandoah
- ZGC 與 Shenandoah —— 現代的低延遲垃圾回收器。
- 目標是達到極低停頓(毫秒等級),即使在巨大堆(達 TB)上也能運作。
- 幾乎完全與應用並行運作。
- 需要 Java 11+(ZGC)或 Java 12+(Shenandoah)。
- 適用於對延遲嚴格要求的系統(交易所、金融科技、即時分析)。
- 啟用旗標:-XX:+UseZGC 或 -XX:+UseShenandoahGC
3. 現代 GC 的工作原理
年輕代與老年代(Young/Old Generation)
JVM 會將堆分成兩個大區塊:
年輕代(Young Generation):所有新建物件都先進入這裡。在此區的回收發生頻繁且快速(Minor GC)。
老年代(Old Generation, Tenured):在年輕代中「存活」了數次回收的物件會被提升至此。這裡的回收較少發生,但耗時更長(Major/Full GC)。
為什麼要這樣劃分?因為大多數 Java 物件的存活時間都很短(例如方法中的暫時字串、集合)。因此,可以頻繁且快速地回收年輕代,而不必動到老年代。
Minor GC
- 只清理年輕代。
- 速度快、停頓短。
- 不影響老年代物件。
Major(Full)GC
- 清理整個堆(包含年輕代與老年代)。
- 可能耗時很久(在大堆上可達數秒甚至更久)。
- 通常伴隨明顯的應用程式停頓。
GC 如何判定哪些物件可以刪除?
GC 從根參考(root set)開始尋找「存活」的物件:例如執行緒堆疊中的區域變數、靜態欄位、方法參數等。凡是「到得了」的都視為存活;其餘即為垃圾。
4. 現代回收器比較:G1、ZGC、Shenandoah
讓我們來理解最流行、最現代的 GC 有何不同。下表為一個直觀比較:
| 回收器 | 主要目標 | 記憶體模型 | 最小停頓 | 可擴展性 | 支援 | 適用情況 |
|---|---|---|---|---|---|---|
| G1 | 在停頓與速度間取得平衡 | 區域 | ~10–200 毫秒 | 可達數百 GB | Java 9+(預設) | 大多數伺服器端應用 |
| ZGC | 最小化停頓 | 區域、「著色標記」 | <10 毫秒 | 可達數 TB | Java 11+ | 即時/對延遲極為敏感 |
| Shenandoah | 最小化停頓 | 區域、「著色標記」 | <10 毫秒 | 可達數 TB | Java 12+(Red Hat) | 即時/對延遲極為敏感 |
G1 GC:Garbage First
- 將堆切分為眾多區域(一般每個 1–32 MB)。
- 在回收時優先選取垃圾最多的區域(「garbage first」)。
- 可以只回收部分堆,而非一次處理全部。
- 可設定目標停頓:-XX:MaxGCPauseMillis=200。
- 適合在效能與停頓之間取得平衡;自 Java 9 起為預設。
啟用範例(若被關閉):
java -XX:+UseG1GC -jar myapp.jar
ZGC:Z Garbage Collector
- 在 Java 11 屬於實驗性,於 Java 15 起成為穩定版。
- 幾乎不會停止應用:即使堆為 1–2 TB,停頓通常 <10 毫秒。
- 使用「著色標記」(coloring)與特殊指標。
- 需要 64 位 JVM;不支援 32 位系統。
- 支援 Linux、macOS、Windows。
啟用範例:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- 由 Red Hat 開發;目標與 ZGC 類似。
- 極小停頓,並與應用高度並行運作。
- 支援 Linux 與 Windows;屬於部分 OpenJDK 發行版的一部分。
- 採用類似技術,但內部演算法不同。
啟用範例:
java -XX:+UseShenandoahGC -jar myapp.jar
視覺化比較
graph TD
A[年輕代] -->|Minor GC| B[老年代]
B -->|Major GC| C[GC Pause]
D[G1: 區域] --> E[選擇性區域]
F[ZGC/Shenandoah: 區域] --> G[並行清理]
5. 實務:如何查詢與變更 GC
如何知道正在使用哪個 GC?
- JVM 日誌: 使用參數 -Xlog:gc*(Java 9+)或 -verbose:gc(Java 8 以前)啟動應用。 在日誌中可以看到使用的 GC 以及停頓頻率。
- jcmd: 執行:
其中 <pid> 為 Java 行程的識別碼。jcmd <pid> VM.flags - jvisualvm: 在「監控」區段可以查看 GC 類型。
如何為你的應用程式變更 GC?
在啟動 Java 程式時加入所需旗標:
G1 GC(預設,可明確指定):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
如何設定堆大小與停頓?
- 最大堆大小:-Xmx2G
- 最小堆大小:-Xms512M
- 對 G1:目標停頓——-XX:MaxGCPauseMillis=200
完整啟動範例:
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. 針對不同場景選擇 GC 的要點
何時選擇 G1
- 在大多數伺服器與桌面應用中——作為預設通常是很好的選擇。
- 在從數百 MB 到數百 GB 的堆上表現良好。
- 在速度與停頓之間取得良好平衡。
何時選擇 ZGC 或 Shenandoah
- 如果應用對延遲敏感(latency‑critical:交易所、線上遊戲、即時分析)。
- 如果堆非常巨大(數百 GB 以上)。
- 如果只能接受極小停頓(毫秒等級)。
- 需要 Java 11+(ZGC)或 Java 12+(Shenandoah)。
何時 Parallel GC 就足夠
- 對小型應用,若重視最大吞吐量、且停頓不關鍵。
- 對批次處理(batch),能接受在 Full GC 時停下來。
7. 範例:在簡單應用上比較 GC 行為
一個會產生大量暫時物件的小程式(模擬訂單處理):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// 每個迴圈建立 100 000 個物件
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// 稍微休息一下
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
使用不同 GC 啟動並查看日誌:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
你會看到什麼?
G1 會產生頻繁但短的停頓。ZGC/Shenandoah——停頓更短,但可能更頻繁。Parallel GC——停頓較長,但發生次數較少。
8. 使用 GC 時的常見錯誤與細節
錯誤 1:以為 GC 能解決所有記憶體問題。 GC 並不是魔法棒。如果你維持著對不需要物件的參考,再強的 GC 也無濟於事——最終會造成記憶體洩漏。
錯誤 2:強制呼叫 System.gc()。 JVM 更清楚何時該回收。強制 GC 可能導致長停頓並降低效能。
錯誤 3:忽視 GC 日誌。 若不檢視 GC 日誌,可能會忽略你的應用程式經常在 Full GC 上「卡住」。
錯誤 4:使用過時的 GC。 例如 CMS 已不再演進。建議改用 G1 或現代的低延遲回收器。
錯誤 5:為任務選錯 GC。 對延遲敏感的應用若使用 Parallel GC——會遇到較長的停頓。若是批次處理卻啟用 ZGC——則會引入不必要的額外開銷。
GO TO FULL VERSION