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

JVM 垃圾回收核心原理深度解析:從可達性分析到分代收集實戰

     編輯:Java 綜合教學

前言

垃圾回收(GC)是 Java 自動內存管理的基石,也是面試高頻考點與性能調優核心戰場。本文系統梳理 GC 的「判定邏輯」、「收集演算法」、「分代策略」三大核心支柱,並結合 HotSpot 實現細節與生產調優視角,構建完整的 GC 知識體系。

一、垃圾判定:誰是「垃圾」?

1.1 引用計數法:簡直但有致命缺陷

  • 原理:對象頭維護計數器,引用 +1,失效 -1,歸零即可回收。
  • 優點:實現簡單、判定高效、即時回收。
  • 致命缺陷: 無法解決循環引用。A→B、B→A 導致計數永不歸零,即使 GC Roots 不可達。
  • 應用:Python、C++ shared_ptr、COM 對象;主流 JVM 未採用。

1.2 可達性分析算法:主流 JVM 的標準答案

  • 核心思想:以 GC Roots 為起點,向下搜索引用鏈,不可達對象判定為垃圾。
  • GC Roots 範圍(HotSpot):
    1. 虛擬機棧局部變量表引用的對象
    2. 方法區類靜態屬性引用的對象
    3. 方法區常量引用的對象
    4. 本地方法棧 JNI 引用的對象
    5. 同步鎖持有的對象、JVM 內部持有的對象等
  • 優雅解決循環引用:孤立島嶼無根可達,直接判死。

1.3 對象「死刑」兩次標記機制

不可達 ≠ 立即回收。對象必須經歷兩次標記才真正「死刑」:

  1. 第一次標記: 可達性分析發現不可達 → 標記 → 判斷是否需執行 finalize()(未重寫或已執行過則跳過)。
  2. 進入 F-Queue: 需執行 finalize() 的對象放入隊列,低優先級 Finalizer 線程異步執行(不保證執行完成、不阻塞 GC)。
  3. 第二次標記: GC 再次掃描 F-Queue:
    • 對象在 finalize() 中重新與 GC Roots 建立關聯(如賦值給靜態字段) → 「復活」 → 移出回收集合。
    • 未復活 → 確定回收。

警示: finalize() 僅執行一次,第二次 GC 直接回收。不建議依賴它釋放資源,請用 try-with-resources / Cleaner (Java 9+)。

二、垃圾收集演算法:三大經典與權衡

演算法核心思想優點缺點適用場景
標記-清除 標記存活 → 清除死亡 實現簡單,無需移動對象 1. 效率低(雙遍歷) 2. 產生內存碎片,大對象分配困難 老年代(配合整理)
複製 內存劃分兩塊等大區域,每次用一塊,GC 時存活對象複製到另一塊 無碎片,分配指針碰撞極快,實現簡單 浪費 50% 內存 新生代(存活率低)
標記-整理 標記存活 → 移動存活對象到一端 → 清理邊界外內存 無碎片,內存利用率高 移動對象需更新引用,開銷大於清除 老年代(存活率高)

三、分代收集理論與 HotSpot 實戰

3.1 核心假設:弱分代假說

  • 絕大多數對象「朝生夕死」(短命)。
  • 熬過越多次 GC 的對象,存活時間越長(長命)。

→ 將堆劃分為新生代、老年代,針對不同特性選擇演算法。

3.2 新生代:複製演算法的優化實踐

  • 劃分:Eden(80%) + Survivor S0/S1(各 10%),默認比例 8:1:1(-XX:SurvivorRatio=8)。
  • 分配:新對象優先 Eden,大對象直接進老年代(-XX:PretenureSizeThreshold)。
  • Minor GC 流程:
    1. Eden + S0 存活對象 → 複製到 S1(To 區)。
    2. 清空 Eden、S0(From 區)。
    3. S0/S1 角色互換。
    4. 年齡計數器 +1,達閾值(默認 15,-XX:MaxTenuringThreshold)晉升老年代。
  • 動態年齡判定:S1 區同齡對象總大小 > S1 容量 50%,直接晉升。
  • 空間擔保:S1 放不下時,直接進老年代(需老年代有空間)。

3.3 老年代:標記-清除/整理混合策略

  • 特點:對象大、存活率高、無額外空間擔保。
  • 演算法組合:
    • CMS / G1:標記-清除(並發階段)+ 標記-整理(壓縮碎片,STW)。
    • Parallel Old:標記-整理(多線程並行)。
    • ZGC / Shenandoah:著色指針/讀屏障 實現並發整理,近乎零停頓。

3.4 永久代 → 元空間

  • Java 7:永久代(堆中),存類元數據、常量、靜態變量,易 OOM(-XX:MaxPermSize)。
  • Java 8+:元空間(本地內存),類元數據移出堆,默認無上限(受物理內存限制),常量/靜態變量移入堆。參數 -XX:MetaspaceSize/MaxMetaspaceSize。

四、七大經典收集器與選型指南

收集器代演算法線程特點適用場景
Serial新/老複製/標記-整理單線程簡單、Client 模式默認、STW單核、小內存、嵌入式
ParNew新複製多線程Serial 多線程版、配合 CMS多核、需低停頓、配合 CMS
Parallel Scavenge新複製多線程吞吐量優先、自適應調節批處理、科學計算、後台任務
Serial Old老標記-整理單線程Serial 老年代版、CMS 後備Client 模式、CMS 失敗後備
Parallel Old老標記-整理多線程Parallel Scavenge 老年代版吞吐量優先、多核
CMS老標記-清除多線程低停頓、並發收集、碎片化、CPU 敏感響應時間敏感、Web 服務(JDK 9 棄用,JDK 14 移除)
G1全堆標記-整理+複製多線程分區、可預測停頓、Region、混合 GC、JDK 9+ 默認大堆(>4G)、多核、均衡吞吐/停頓、現代首選

五、GC 日誌分析與調優實戰入門

5.1 關鍵日誌參數

# JDK 8-
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

# JDK 9+ (統一日誌框架)
-Xlog:gc*:file=gc.log:time,uptime,level,tags

5.2 核心指標關注點

  • 停頓時間: Young GC / Full GC 耗時,是否超過 SLA。
  • 頻率: Young GC 間隔、Full GC 觸發頻率(頻繁 Full GC 通常意味著老年代空間不足或內存洩漏)。
  • 內存回收率: GC 前後堆佔用變化,老年代增長趨勢。
  • 擴容/晉升細節: 幸存區使用率、對象晉升年齡分布。

5.3 常見調優切入點

  1. 新生代過小: 頻繁 Minor GC,對象來不及在新生代死亡就被擠進老年代 → 調大 -Xmn 或 -XX:SurvivorRatio。
  2. 大對象直接進老年代: 調整 -XX:PretenureSizeThreshold 或優化代碼避免大數組/集合。
  3. 長命對象過早晉升: 調大 -XX:MaxTenuringThreshold 或優化幸存區大小。
  4. 老年代碎片/Full GC 頻繁: 切換 G1/ZGC,或啟用 CMS 壓縮(-XX:+UseCMSCompactAtFullCollection)。
  5. 內存洩漏: MAT 分析 Heap Dump,定位 GC Roots 強引用鏈。

六、總結

JVM GC 是一套精密的工程權衡系統:可達性分析判生死,三大演算法權衡時空,分代假說指導區域劃分,收集器實現具體策略。理解其底層邏輯,而非死記參數,才能在面對 OOM、停頓抖動、吞吐下降等生產疑難雜症時,從容定位根因,精準施策。建議以 G1 為起點(JDK 11+ 默認),逐步探索 ZGC/Shenandoah 等新一代低延遲收集器,構建現代化 Java 服務的內存管理基石。

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