垃圾回收(Garbage Collection,GC)是 JVM 自動記憶體管理的核心機制。理解 GC 的原理和各種回收器的特點,對於效能調校和問題排查至關重要。

如何判斷物件是否存活#

引用計數法(Reference Counting)#

// 引用計數法的原理
Object a = new Object();  // Object 引用計數 = 1
Object b = a;             // Object 引用計數 = 2
b = null;                 // Object 引用計數 = 1
a = null;                 // Object 引用計數 = 0可回收

引用計數法無法解決循環參照問題。JVM 主流實作都不採用此方法。

// 循環參照範例
class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;  // a 參照 b
b.next = a;  // b 參照 a

a = null;
b = null;
// 雖然 a、b 都為 null,但兩個 Node 物件互相參照
// 引用計數都不為 0無法回收

可達性分析(Reachability Analysis)#

JVM 採用可達性分析演演算法來判斷物件是否存活。

flowchart TD
    GC[GC Roots] --> A
    GC --> B
    GC --> C
    A --> B --> C
    B --> D

    E <--> F

    subgraph reachable [可達物件]
        A
        B
        C
        D
    end

    subgraph unreachable [不可達,待回收]
        E
        F
    end

    style unreachable fill:#ffcccc

GC Roots 包括:

  1. 虛擬機器堆疊(堆疊幀中的區域變數表)中參照的物件
  2. 方法區中類別靜態屬性參照的物件
  3. 方法區中常數參照的物件
  4. 本地方法堆疊中 JNI(Native 方法)參照的物件
  5. JVM 內部的參照(如基本型別對應的 Class 物件、常駐例外物件等)
  6. 被同步鎖(synchronized)持有的物件
  7. JMXBean、JVMTI 中註冊的回呼、本地程式碼快取等

四種參照類型#

// 1. 強參照(Strong Reference)- 絕不回收
Object strong = new Object();

// 2. 軟參照(Soft Reference)- 記憶體不足時回收
SoftReference<Object> soft = new SoftReference<>(new Object());
// 適合實作快取

// 3. 弱參照(Weak Reference)- 下次 GC 必定回收
WeakReference<Object> weak = new WeakReference<>(new Object());
// WeakHashMap 的實作原理

// 4. 虛參照(Phantom Reference)- 用於追蹤物件被回收的時機
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
// 無法透過虛參照取得物件
軟參照快取範例
public class SoftCache<K, V> {
    private final Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();

    public void put(K key, V value) {
        cache.put(key, new SoftReference<>(value));
    }

    public V get(K key) {
        SoftReference<V> ref = cache.get(key);
        if (ref != null) {
            V value = ref.get();
            if (value == null) {
                // 軟參照已被回收
                cache.remove(key);
            }
            return value;
        }
        return null;
    }
}

Stop-the-World 與安全點#

Stop-the-World(STW)#

GC 執行時需要暫停所有應用程式執行緒,這就是 Stop-the-World。STW 時間過長會導致應用程式回應延遲,是 GC 調校的主要目標。

安全點(Safepoint)#

JVM 只有在安全點才能暫停執行緒執行 GC。

安全點的位置:
├── 方法呼叫時
├── 迴圈跳轉時(非計數迴圈的回邊)
├── 例外跳轉時
└── JNI 方法執行時
// 計數迴圈可能導致長時間無法進入安全點
public void countLoop() {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
        // 這裡預設沒有安全點檢測
        // 可能導致其他執行緒等待很久
    }
}

// 解決方案:使用 -XX:+UseCountedLoopSafepoints(Java 10+)
// 或改用非計數迴圈
安全點測試範例
// 體驗計數迴圈導致的長暫停
// java -XX:+PrintGCApplicationStoppedTime SafepointTest
public class SafepointTest {
    static double sum = 0;

    public static void foo() {
        for (int i = 0; i < 0x77777777; i++) {
            sum += Math.sqrt(i);
        }
    }

    public static void bar() {
        for (int i = 0; i < 50_000_000; i++) {
            new Object().hashCode();
        }
    }

    public static void main(String[] args) {
        new Thread(SafepointTest::foo).start();
        new Thread(SafepointTest::bar).start();
    }
}

三種基礎回收演演算法#

1. 標記-清除(Mark-Sweep)#

回收前:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │   │ C │ D │   │ E │   │
└───┴───┴───┴───┴───┴───┴───┴───┘
      ↑ 存活     ↑ 存活

標記後清除:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│   │ B │   │   │ D │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┘
      ↑           ↑
   存活物件     存活物件

優點: 實作簡單 缺點: 產生記憶體碎片,分配效率低

2. 標記-複製(Mark-Copy)#

回收前(只使用 From 區):
From: ┌───┬───┬───┬───┐    To: ┌───┬───┬───┬───┐
      │ A │ B │ C │ D │        │   │   │   │   │
      └───┴───┴───┴───┘        └───┴───┴───┴───┘
          ↑       ↑
        存活    存活

複製後(交換 From 和 To):
From: ┌───┬───┬───┬───┐    To: ┌───┬───┬───┬───┐
      │   │   │   │   │        │ B │ D │   │   │
      └───┴───┴───┴───┘        └───┴───┴───┴───┘
                                   ↑ 緊湊排列

優點: 無碎片,分配快速(指標碰撞) 缺點: 記憶體使用率只有 50%

3. 標記-整理(Mark-Compact)#

回收前:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │   │ C │ D │   │ E │   │
└───┴───┴───┴───┴───┴───┴───┴───┘
      ↑       ↑
    存活    存活

整理後:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ B │ D │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┘
  ↑   ↑   ↑
存活  存活 空閒連續空間

優點: 無碎片,記憶體使用率高 缺點: 需要移動物件,效能開銷大

分代回收#

世代假說#

分代回收基於兩個假說:

  1. 弱世代假說:絕大多數物件都是朝生夕死的
  2. 強世代假說:熬過越多次 GC 的物件越不容易死亡
┌─────────────────────────────────────────────────────────────┐
│                          堆積區                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────┐  ┌───────────────────────┐│
│  │        新生代 (Young)        │  │      老年代 (Old)      ││
│  │                             │  │                       ││
│  │  採用複製演演算法               │  │  採用標記-整理演演算法   ││
│  │  Minor GC 頻繁但快速         │  │  Major GC 較慢         ││
│  │                             │  │                       ││
│  │  ┌─────────────────────┐   │  │                       ││
│  │  │       Eden          │   │  │                       ││
│  │  │   物件首先分配於此    │   │  │                       ││
│  │  └─────────────────────┘   │  │                       ││
│  │  ┌─────────┐ ┌─────────┐   │  │                       ││
│  │  │   S0    │ │   S1    │   │  │                       ││
│  │  │Survivor│ │Survivor│   │  │                       ││
│  │  └─────────┘ └─────────┘   │  │                       ││
│  └─────────────────────────────┘  └───────────────────────┘│
│                                                             │
└─────────────────────────────────────────────────────────────┘

Minor GC 流程#

flowchart TD
    A[Eden 區滿] --> B[觸發 Minor GC]
    B --> C[標記 Eden 和 S0 中存活物件]
    C --> D[複製存活物件到 S1]
    D --> E[物件年齡 +1]
    E --> F[清空 Eden 和 S0]
    F --> G[交換 S0 和 S1]
    G --> H{年齡達到閾值?}
    H -->|是| I[晉升到老年代]
    H -->|否| J[留在 Survivor]

卡表(Card Table)與寫屏障#

跨代參照問題:Minor GC 時,老年代物件可能參照新生代物件。卡表用於避免掃描整個老年代。

┌───────────────────────────────────────────────────────┐
│                      老年代                           │
│  ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐  │
│  │Card0│Card1│Card2│Card3│Card4│Card5│Card6│Card7│  │
│  │     │ ●   │     │     │ ●   │     │     │     │  │
│  └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘  │
│          │                  │                        │
│          └───────┬──────────┘                        │
│                  │ 參照新生代物件                     │
└──────────────────│──────────────────────────────────┘
┌───────────────────────────────────────────────────────┐
│                      新生代                           │
└───────────────────────────────────────────────────────┘

卡表:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┘
      ↑               ↑
   Dirty Card    Dirty Card
   (需要掃描)

垃圾回收器#

回收器分類#

┌─────────────────────────────────────────────────────────────────────┐
│                         垃圾回收器演進                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  新生代回收器              老年代回收器              整堆回收器        │
│  ┌────────────┐          ┌────────────┐          ┌────────────┐    │
│  │   Serial   │ ←───────→│ Serial Old │          │     G1     │    │
│  │  (串列)     │          │  (串列)     │          │ (區域化分代) │    │
│  └────────────┘          └────────────┘          └────────────┘    │
│                                                                     │
│  ┌────────────┐          ┌────────────┐          ┌────────────┐    │
│  │  ParNew    │ ←───────→│    CMS     │          │    ZGC     │    │
│  │  (平行)     │          │ (並行標記)  │          │ (低延遲)    │    │
│  └────────────┘          └────────────┘          └────────────┘    │
│                                                                     │
│  ┌────────────┐          ┌────────────┐          ┌────────────┐    │
│  │ Parallel   │ ←───────→│ Parallel   │          │ Shenandoah │    │
│  │ Scavenge   │          │    Old     │          │ (低延遲)    │    │
│  │ (吞吐量優先) │          │ (吞吐量優先) │          │            │    │
│  └────────────┘          └────────────┘          └────────────┘    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Serial / Serial Old#

# 啟用 Serial GC
-XX:+UseSerialGC
  • 特點:單執行緒,STW 時間長
  • 適用場景:單核心 CPU、小型應用、Client 模式

Parallel Scavenge / Parallel Old#

# 啟用 Parallel GC(Java 8 預設)
-XX:+UseParallelGC
-XX:+UseParallelOldGC

# 設定 GC 執行緒數
-XX:ParallelGCThreads=4

# 最大 GC 暫停時間目標(毫秒)
-XX:MaxGCPauseMillis=200

# 吞吐量目標(GC 時間佔比)
-XX:GCTimeRatio=99
  • 特點:多執行緒,關注吞吐量
  • 適用場景:後台運算、批次處理

CMS(Concurrent Mark Sweep)#

# 啟用 CMS(已棄用,Java 14 移除)
-XX:+UseConcMarkSweepGC

# 並行 GC 執行緒數
-XX:ParallelCMSThreads=4

# 老年代使用率達到多少觸發 CMS
-XX:CMSInitiatingOccupancyFraction=70

CMS 四階段:

sequenceDiagram
    participant App as 應用程式
    participant GC as GC 執行緒

    Note over App,GC: 1. 初始標記 (STW)
    App-xGC: 暫停
    GC->>GC: 標記 GC Roots 直接關聯物件
    App->>App: 恢復執行

    Note over App,GC: 2. 並行標記
    par 並行執行
        App->>App: 正常運行
        GC->>GC: 標記所有可達物件
    end

    Note over App,GC: 3. 重新標記 (STW)
    App-xGC: 暫停
    GC->>GC: 修正並行標記期間的變動
    App->>App: 恢復執行

    Note over App,GC: 4. 並行清除
    par 並行執行
        App->>App: 正常運行
        GC->>GC: 清除垃圾物件
    end

CMS 的問題:

  • 記憶體碎片(使用標記-清除)
  • 浮動垃圾(並行清除時產生的新垃圾)
  • Concurrent Mode Failure(老年代放不下時退化為 Serial Old)

G1(Garbage First)#

# 啟用 G1 GC(Java 9+ 預設)
-XX:+UseG1GC

# 設定期望的最大暫停時間
-XX:MaxGCPauseMillis=200

# Region 大小(1MB ~ 32MB,必須是 2 的冪次)
-XX:G1HeapRegionSize=4m

# 老年代佔比觸發混合回收
-XX:InitiatingHeapOccupancyPercent=45

G1 的 Region 設計:

┌────────────────────────────────────────────────────────────────┐
│                        G1 堆積佈局                              │
├────────────────────────────────────────────────────────────────┤
│  ┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐            │
│  │ E  ││ E  ││ S  ││ O  ││ O  ││ H  ││ H  ││ 空 │            │
│  └────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘            │
│  ┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐            │
│  │ O  ││ 空 ││ E  ││ S  ││ O  ││ 空 ││ O  ││ E  │            │
│  └────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘            │
│                                                                │
│  E = Eden    S = Survivor    O = Old    H = Humongous         │
│                                                                │
│  Region 可以動態變化身份,不再固定分配給某一世代                  │
└────────────────────────────────────────────────────────────────┘

G1 回收過程:

Young GC:
├── 選擇所有 Eden 和 Survivor Region
├── 複製存活物件到新的 Survivor 或 Old Region
└── STW,但通常很快

Mixed GC:
├── 初始標記(STW)
├── 並行標記
├── 最終標記(STW)
├── 篩選回收(STW)
│   └── 選擇回收價值最高的 Region(垃圾最多)
└── 優先回收垃圾比例高的 Region

ZGC#

# 啟用 ZGC(Java 11+ 實驗性,Java 15+ 正式)
-XX:+UseZGC

# 設定堆積大小
-Xmx16g -Xms16g

# 並行 GC 執行緒數
-XX:ConcGCThreads=4

ZGC 特點:

  • 暫停時間不超過 10ms(通常 < 1ms)
  • 暫停時間不隨堆積大小增加
  • 支援 TB 級堆積
  • 使用染色指標(Colored Pointers)和讀屏障
ZGC vs G1 比較
特性G1ZGC
最大暫停時間數十到數百毫秒< 10ms
堆積大小影響堆積越大暫停越長幾乎不受影響
記憶體佔用一般稍高(染色指標)
CPU 佔用一般稍高(讀屏障)
適用場景通用低延遲要求
Java 版本9+15+

GC 日誌分析#

啟用 GC 日誌#

# Java 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log

# Java 9+
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags

G1 GC 日誌範例#

[2024-01-15T10:30:45.123+0800][0.234s][info][gc,start     ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
[2024-01-15T10:30:45.125+0800][0.236s][info][gc,task      ] GC(0) Using 4 workers of 4 for evacuation
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,phases    ] GC(0)   Pre Evacuate Collection Set: 0.1ms
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,phases    ] GC(0)   Evacuate Collection Set: 2.5ms
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,phases    ] GC(0)   Post Evacuate Collection Set: 0.3ms
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,phases    ] GC(0)   Other: 0.2ms
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,heap      ] GC(0) Eden regions: 10->0(8)
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,heap      ] GC(0) Survivor regions: 0->2(2)
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,heap      ] GC(0) Old regions: 0->1
[2024-01-15T10:30:45.128+0800][0.239s][info][gc,heap      ] GC(0) Humongous regions: 0->0
[2024-01-15T10:30:45.128+0800][0.239s][info][gc           ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 10M->3M(64M) 3.1ms

日誌解讀:

  • Pause Young (Normal):Young GC
  • 10M->3M(64M):GC 前 10M → GC 後 3M,堆積總大小 64M
  • 3.1ms:GC 暫停時間

GC 調校策略#

調校目標#

flowchart TD
    A[選擇 GC 策略] --> B{主要目標?}
    B -->|低延遲| C{Java 版本?}
    B -->|高吞吐量| D[Parallel GC]
    B -->|低記憶體| E[Serial GC]

    C -->|Java 15+| F[ZGC]
    C -->|Java 9-14| G[G1 GC]
    C -->|Java 8| H[CMS]

    D --> I[適合批次處理]
    E --> J[適合小型應用]
    F --> K[暫停 < 10ms]
    G --> L[暫停 < 200ms]
    H --> M[已棄用,遷移到 G1]
目標說明適用場景
低延遲減少 GC 暫停時間即時系統、交易系統
高吞吐量減少 GC 總時間批次處理、後台計算
低記憶體佔用減少堆積大小資源受限環境

常見調校方向#

# 1. 調整堆積大小
-Xms4g -Xmx4g  # 固定大小避免動態調整

# 2. 調整新生代比例
-XX:NewRatio=2  # 老年代:新生代 = 2:1
-Xmn2g          # 直接設定新生代大小

# 3. 調整 Survivor 比例
-XX:SurvivorRatio=8  # Eden:Survivor = 8:1

# 4. 調整晉升閾值
-XX:MaxTenuringThreshold=15  # 預設 15

# 5. G1 調校
-XX:MaxGCPauseMillis=100  # 目標暫停時間
-XX:InitiatingHeapOccupancyPercent=45  # 觸發併發標記

# 6. 啟用大頁
-XX:+UseLargePages

調校的首要原則:先監控,再調校。使用 GC 日誌、jstat、VisualVM 等工具收集資料,找出問題再針對性最佳化。避免盲目調參。

總結#

垃圾回收的核心要點:

  1. 判斷存活:可達性分析,四種參照類型
  2. 基礎演演算法:標記-清除、標記-複製、標記-整理
  3. 分代回收:基於世代假說,新生代用複製,老年代用整理
  4. 回收器選擇
    • 小堆積/單核心:Serial
    • 高吞吐量:Parallel
    • 低延遲(舊版):CMS
    • 通用推薦:G1(Java 9+ 預設)
    • 超低延遲:ZGC/Shenandoah
  5. 調校重點:明確目標,監控先行,逐步最佳化