垃圾回收(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:#ffccccGC Roots 包括:
- 虛擬機器堆疊(堆疊幀中的區域變數表)中參照的物件
- 方法區中類別靜態屬性參照的物件
- 方法區中常數參照的物件
- 本地方法堆疊中 JNI(Native 方法)參照的物件
- JVM 內部的參照(如基本型別對應的 Class 物件、常駐例外物件等)
- 被同步鎖(synchronized)持有的物件
- 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 │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
↑ ↑ ↑
存活 存活 空閒連續空間優點: 無碎片,記憶體使用率高 缺點: 需要移動物件,效能開銷大
分代回收#
世代假說#
分代回收基於兩個假說:
- 弱世代假說:絕大多數物件都是朝生夕死的
- 強世代假說:熬過越多次 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=70CMS 四階段:
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: 清除垃圾物件
endCMS 的問題:
- 記憶體碎片(使用標記-清除)
- 浮動垃圾(並行清除時產生的新垃圾)
- 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=45G1 的 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(垃圾最多)
└── 優先回收垃圾比例高的 RegionZGC#
# 啟用 ZGC(Java 11+ 實驗性,Java 15+ 正式)
-XX:+UseZGC
# 設定堆積大小
-Xmx16g -Xms16g
# 並行 GC 執行緒數
-XX:ConcGCThreads=4ZGC 特點:
- 暫停時間不超過 10ms(通常 < 1ms)
- 暫停時間不隨堆積大小增加
- 支援 TB 級堆積
- 使用染色指標(Colored Pointers)和讀屏障
ZGC vs G1 比較
| 特性 | G1 | ZGC |
|---|---|---|
| 最大暫停時間 | 數十到數百毫秒 | < 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,tagsG1 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 GC10M->3M(64M):GC 前 10M → GC 後 3M,堆積總大小 64M3.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 等工具收集資料,找出問題再針對性最佳化。避免盲目調參。
總結#
垃圾回收的核心要點:
- 判斷存活:可達性分析,四種參照類型
- 基礎演演算法:標記-清除、標記-複製、標記-整理
- 分代回收:基於世代假說,新生代用複製,老年代用整理
- 回收器選擇:
- 小堆積/單核心:Serial
- 高吞吐量:Parallel
- 低延遲(舊版):CMS
- 通用推薦:G1(Java 9+ 預設)
- 超低延遲:ZGC/Shenandoah
- 調校重點:明確目標,監控先行,逐步最佳化