首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 垃圾回收機制深度解析:從引用計數到標記-清除、分代收集、ZGC

     編輯:Java 相關知識

垃圾回收 (GC) 是 Java 自動內存管理的基石。理解 GC 原理,能排查 OOM、優化停頓、選擇適合業務的收集器。本文從手動內存管理對比入手,層層遞進講解引用計數、標記-清除、分代假設、主流收集器 (Serial/Parallel/CMS/G1/ZGC/Shenandoah)、JVM 參數調優實戰。

一、為什麼需要 GC?手動管理的痛點

// 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;
}
  • 內存洩漏:忘記釋放、異常路徑未釋放
  • 懸空指針:釋放後繼續訪問
  • 雙重釋放:多處釋放同一塊內存
  • GC 將「釋放時機」決策權交給運行時,開發者專注業務邏輯

二、引用計數:簡單但有缺陷

  • 每個對象維護計數器,引用 +1,失效 -1,歸 0 即回收
  • 採用語言:Python、PHP、Perl、Swift (ARC)、C++ shared_ptr
  • 致命缺陷:循環引用:A→B、B→A,計數永不歸 0 → 內存洩漏
  • 補救:弱引用、周期性循環檢測算法 (Python)、所有權語義 (Rust)

三、JVM 採用:可達性分析 (標記-清除基礎)

GC Roots (根節點)

作為「存活」判定起點:

  1. 虛棧棧幀中的局部變量 (參數、臨時變量)
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象 (字符串常量池等)
  4. 本地方法棧 JNI 引用的對象
  5. 活躍線程對象
  6. 同步鎖持有的對象
  7. JVM 內部對象 (Class 對象、異常對象、系統類加載器等)

標記-清除兩階段

// 概念演示
Phase 1: Mark (標記)
  從 GC Roots 出發,DFS/BFS 遍歷對象圖
  到達的對象 = 存活 (標記 bit=1)
  未到達 = 垃圾 (bit=0)

Phase 2: Sweep (清除)
  掃描堆內存,回收未標記對象佔用空間
  更新空閒列表 / 指針碰撞指針

三大缺陷

  1. 效率低:需遍歷全堆,標記/清除均 O(N)
  2. 內存碎片:清除後產生不連續空閒塊,大對象分配困難
  3. Stop-The-World (STW):標記/清除期間應用線程全部暫停

四、演進:標記-複製、標記-整理、分代收集

算法核心思想優點缺點應用區域
標記-複製劃分 From/To 兩區,存活對象複製到 To,清空 From無碎片、分配快 (指針碰撞)內存利用率 50%、大對象複製開銷大新生代 (Eden/Survivor)
標記-整理標記後將存活對象向一端移動,整理出連續空間無碎片、利用率高移動對象需更新引用、STW 長老年代
分代收集新生代用複製、老年代用整理,針對不同存活特性平衡效率與利用率、業界標準實現複雜、需處理跨代引用HotSpot 默認策略

五、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+超大堆、低延遲、開源生態

關鍵概念區分

  • 並行:多條 GC 線程同時工作 (用戶線程暫停)
  • 並發:GC 線程與用戶線程交替/同時運行 (用戶線程不完全暫停)
  • 吞吐量 = 用戶代碼時間 / (用戶代碼時間 + GC 時間)
  • 停頓時間:STW 導致用戶線程凍結的時間

七、G1 收集器深度解析 (JDK 9+ 默認)

Region 區域化設計

  • 堆劃分為 2048 個大小相等的 Region (1MB~32MB,2 的冪次)
  • Region 動態扮演 Eden、Survivor、Old、Humongous (大對象) 角色
  • 不再物理連續劃分代,邏輯分代

收集流程

  1. Young GC:Eden 滿 → 掃描 Eden+Survivor Region → 複製存活到新 Survivor/Old Region
  2. Mixed GC:老年代佔比達閾值 (默認 45%) → 掃描全堆 → 回收部分 Old Region + Young Region
  3. Full GC:Mixed GC 來不及/併發失敗 → 退化單線程 Serial Full GC (兜底)

關鍵參數

-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

八、ZGC/Shenandoah:亞毫秒停頓新紀元

ZGC 核心技術:染色指針

  • 64 位指針高位存儲元數據:Finalizable(1)、Remapped(1)、Marked0/1(2)
  • 讀屏障:讀取對象引用時自動修正指針 (轉發)
  • 並發階段:標記、重定位、重映射,幾乎全程並發
  • 支持 TB 級堆,停頓 < 1ms,與堆大小無關

啟用 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 (實驗)

九、GC 日誌分析與調優實戰

啟用統一日誌格式 (JDK 9+)

-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m

關鍵指標

  • Young GC 頻率/耗時:過頻 → 增大新生代/調整 Survivor
  • Old GC / Mixed GC 頻率/耗時:過久 → 調整 InitiatingHeapOccupancyPercent、MaxGCPauseMillis
  • Full GC 觸發原因:System.gc()、老年代 OOM、Metaspace 滿、並發失敗
  • 分配速率:MB/s,結合業務流量評估

常見調優路徑

  1. 確定目標:低延遲 (G1/ZGC) vs 高吞吐 (Parallel)
  2. 基線測試:壓測獲取 GC 日誌
  3. 調整堆大小:-Xms -Xmx 設同值,避免動態擴縮容
  4. 調整新生代/老年代比例:-XX:NewRatio (G1 無效,靠自適應)
  5. 調整目標停頓:-XX:MaxGCPauseMillis (G1/ZGC)
  6. 大對象優化:-XX:PretenureSizeThreshold 直接進老年代
  7. 驗證效果:對比 GC 頻率、停頓、吞吐、CPU 佔用

十、避坑指南

誤區真相
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 管理資源,才是治本之道。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved