CodeGym /課程 /JAVA 25 SELF /垃圾回收器:G1、ZGC、Shenandoah,比較

垃圾回收器:G1、ZGC、Shenandoah,比較

JAVA 25 SELF
等級 64 , 課堂 1
開放

1. 垃圾回收器(GC)入門

如果你曾經用 C 或 C++ 撰寫過程式,應該對使用 free()delete 手動釋放記憶體很熟悉。在 Java 中就簡單多了:你用 new 建立物件,無需手動刪除——有一位專門的「清潔工」稱為垃圾回收器(Garbage CollectorGC)會處理這件事。

GCJVM 的一部分,會自動釋放那些已經沒有任何參考指向的物件所佔用的記憶體。有了它,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

  • ZGCShenandoah —— 現代的低延遲垃圾回收器。
  • 目標是達到極低停頓(毫秒等級),即使在巨大堆(達 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?

  1. JVM 日誌: 使用參數 -Xlog:gc*(Java 9+)或 -verbose:gc(Java 8 以前)啟動應用。 在日誌中可以看到使用的 GC 以及停頓頻率。
  2. jcmd: 執行:
    jcmd <pid> VM.flags
    
    其中 <pid> 為 Java 行程的識別碼。
  3. 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——則會引入不必要的額外開銷。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION