垃圾回收 (GC) 是 Java 自動內存管理的基石。理解 GC 原理,能排查 OOM、優化停頓、選擇適合業務的收集器。本文從手動內存管理對比入手,層層遞進講解引用計數、標記-清除、分代假設、主流收集器 (Serial/Parallel/CMS/G1/ZGC/Shenandoah)、JVM 參數調優實戰。
// C 手動內存管理
int send_request() {
size_t n = read_size();
int *elements = malloc(n * sizeof(int));
if (read_elements(n, elements) < n) {
// 忘記 free(elements) → 內存洩漏
return -1;
}
free(elements);
return 0;
}
// C++ 智能指針 (引用計數)
int send_request() {
size_t n = read_size();
auto elements = make_shared<vector<int>>();
if (read_elements(n, elements) < n) {
return -1; // 離開作用域自動釋放
}
return 0;
}
作為「存活」判定起點:
// 概念演示
Phase 1: Mark (標記)
從 GC Roots 出發,DFS/BFS 遍歷對象圖
到達的對象 = 存活 (標記 bit=1)
未到達 = 垃圾 (bit=0)
Phase 2: Sweep (清除)
掃描堆內存,回收未標記對象佔用空間
更新空閒列表 / 指針碰撞指針
| 算法 | 核心思想 | 優點 | 缺點 | 應用區域 |
|---|---|---|---|---|
| 標記-複製 | 劃分 From/To 兩區,存活對象複製到 To,清空 From | 無碎片、分配快 (指針碰撞) | 內存利用率 50%、大對象複製開銷大 | 新生代 (Eden/Survivor) |
| 標記-整理 | 標記後將存活對象向一端移動,整理出連續空間 | 無碎片、利用率高 | 移動對象需更新引用、STW 長 | 老年代 |
| 分代收集 | 新生代用複製、老年代用整理,針對不同存活特性 | 平衡效率與利用率、業界標準 | 實現複雜、需處理跨代引用 | HotSpot 默認策略 |
Heap
├── Young Generation (新生代) ~ 1/3 Heap
│ ├── Eden (伊甸區) ~ 80% Young
│ ├── Survivor 0 (S0) ~ 10% Young
│ └── Survivor 1 (S1) ~ 10% Young
└── Old Generation (老年代) ~ 2/3 Heap
對象生命週期:
new Object() → Eden
Minor GC → 存活 → S0/S1 (年齡+1)
年齡 ≥ MaxTenuringThreshold (默認 15) → 老年代
大對象 (-XX:PretenureSizeThreshold) → 直接進老年代
動態年齡判斷 → Survivor 相同年齡總和 > Survivor 一半 → 進老年代
| 收集器 | 區域 | 算法 | 並行/並發 | 特點 | 適用場景 |
|---|---|---|---|---|---|
| Serial | 新/老 | 複製/整理 | 串行 | 單線程、STW、客戶端模式默認 | 單核、小內存、Client 模式 |
| ParNew | 新 | 複製 | 並行 | Serial 多線程版、配合 CMS | 多核、需配合 CMS |
| Parallel Scavenge | 新 | 複製 | 並行 | 吞吐量優先、自適應調節 | 批處理、科學計算、Server 默認 (JDK 8) |
| Parallel Old | 老 | 整理 | 並行 | Parallel Scavenge 老年代版 | 配合 Parallel Scavenge |
| CMS | 老 | 標記-清除 | 並發 | 低停頓、碎片、CPU 敏感、JDK 9 棄用、JDK 14 移除 | 響應時間敏感、舊版本維護 |
| G1 | 全堆 | 標記-整理+複製 | 並行+並發 | 區域化、可預測停頓、JDK 9+ 默認 | 大內存 (6G+)、混合負載、現代首選 |
| ZGC | 全堆 | 染色指針+讀屏障 | 並發 | 亞毫秒停頓、TB 級堆、JDK 11+ 實驗、JDK 15+ 正式 | 超大堆、極低延遲要求 |
| Shenandoah | 全堆 | 連接矩陣+寫屏障 | 並發 | 亞毫秒停頓、Red Hat 主導、JDK 12+ | 超大堆、低延遲、開源生態 |
-XX:+UseG1GC # 啟用 G1
-XX:MaxGCPauseMillis=200 # 目標停頓時間 (默認 200ms)
-XX:G1HeapRegionSize=16m # Region 大小 (1-32MB)
-XX:InitiatingHeapOccupancyPercent=45 # 觸發 Mixed GC 老年代佔比
-XX:G1MixedGCCountTarget=8 # Mixed GC 目標次數
-XX:+ParallelRefProcEnabled # 並行引用處理
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC # 切換 ZGC
# JDK 11-14 (實驗)
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g -Xms16g
# JDK 15+ (正式)
-XX:+UseZGC -Xmx16g -Xms16g
# 關鍵調優
-XX:ZCollectionInterval=60 # 最小 GC 間隔 (秒)
-XX:ZAllocationSpikeTolerance=2 # 分配尖峰容忍因子
-XX:+ZGenerational # JDK 21+ 分代 ZGC (實驗)
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
InitiatingHeapOccupancyPercent、MaxGCPauseMillis-Xms -Xmx 設同值,避免動態擴縮容-XX:NewRatio (G1 無效,靠自適應)-XX:MaxGCPauseMillis (G1/ZGC)-XX:PretenureSizeThreshold 直接進老年代| 誤區 | 真相 |
|---|---|
System.gc() 能強制 GC | 僅建議,JVM 可忽略;生產禁用 |
| 越大堆越好 | 大堆 → Full GC 停頓極長;適度即可 |
| CMS 是最佳低延遲選擇 | 已移除;G1/ZGC 替代 |
| 對象池能減輕 GC | 簡單對象池反而增加老年代壓力;慎用 |
| finalize() 可靠釋放資源 | 不可靠、性能差、JDK 9 棄用;用 try-with-resources / Cleaner |
總結:GC 從引用計數演進至可達性分析、分代收集、區域化 (G1)、染色指針 (ZGC)。現代選型:G1 通用首選 (JDK 9+ 默認)、ZGC/Shenandoah 亞毫秒極致低延遲。調優核心:明確目標、基線測試、小步調參、壓測驗證。代碼層面:減少對象分配、縮短生命週期、避免大對象、善用對象復用、try-with-resources 管理資源,才是治本之道。