JVM 垃圾回收機制深度解析(含圖解)
一、垃圾回收整體流程
垃圾回收圖解
二、垃圾對象判定算法
以下是對 JVM 垃圾回收機制的進一步細化,結合圖解直觀展示核心概念、算法流程及收集器工作原理:
2.1 引用計數法(Reference Counting)
原理:
-
計數器管理
- 每個對象內嵌一個引用計數器(Reference Counter)。
- 引用增加(如被賦值、作為參數傳遞) → 計數器+1。
- 引用失效(如變量置空、離開作用域) → 計數器-1。
-
回收條件
- 當計數器歸零時,對象立即被標記為垃圾并回收。
循環引用問題: 無法處理循環引用(如對象 A 和 B 互相引用),導致內存泄漏。
場景描述:
對象A和B互相引用,導致兩者的引用計數始終≥1,即使它們已不被其他對象引用,也無法被回收。后果:內存泄漏(Memory Leak)。
解決循環引用的常見方法
-
弱引用(Weak Reference)
弱引用不增加對象的引用計數(如Java的WeakReference、Python的weakref)。
-
手動斷開引用
開發者需在代碼中顯式解除循環引用(易出錯,不推薦)。
-
周期檢測算法
定期運行垃圾回收器檢測循環引用(如Python的gc模塊)。
2.2 可達性分析(Reachability Analysis)
可達性分析示意圖
原理:
從一組稱為GC Roots的根對象出發,沿著引用鏈向下搜索,所有能被訪問到的對象視為“存活”,未被訪問的則判定為“不可達”對象,可被回收。
整體結構
外層是可達性分析的主流程,內嵌三色標記法的核心流程。
顏色標識統一:
白色:未處理或可回收對象(最終狀態)
灰色:待處理對象(中間狀態)
黑色 :已處理完成對象
紅色邊框:垃圾回收相關操作
關鍵流程節點
-
標記GC Roots: 從GC Roots(根對象如棧、靜態變量等)出發,遞歸遍歷所有可達對象,將其直接引用的堆對象標記為灰色,并加入灰色隊列,作為標記的起點。
-
三色標記循環: 通過灰色隊列迭代處理對象(白→灰→黑狀態變化)。
- 取出灰色對象,遍歷其引用的子對象;
- 若子對象為白色,標記為灰色并加入隊列;
- 當前灰色對象標記為黑色;
- 重復直到灰色隊列為空。”
-
回收階段: 清除所有白色(未被標記/不可達)對象,釋放內存。
顏色狀態映射
存活對象標記流程:
GC Roots → 白色 → 灰色 → 遍歷引用 → 黑色(完成)垃圾對象:
始終為白色 → 最終被回收
可達性分析的優勢
-
解決循環引用問題
即使對象A和對象B互相引用,若它們無法從GC Roots到達,仍會被判定為垃圾。 -
高效性
通過引用鏈遍歷,僅需處理存活對象,避免全堆掃描。
并發標記的挑戰
在并發標記階段(如CMS、G1回收器),用戶線程可能修改對象引用,導致兩種問題:
-
漏標(Missing Mark)
-
增量更新(Incremental Update)
若黑色對象新增對白色對象的引用,將該黑色對象重新標記為灰色(需重新掃描)。
-
原始快照(SATB)
記錄標記開始時的對象引用關系,后續新增的引用關系視為“待處理”。
-
-
多標(Floating Garbage)
- 已標記為存活的對象被用戶線程置為不可達(通常容忍到下次GC處理)。
GC Roots 包括:
1. 虛擬機棧(Java 棧)中的引用對象
來源: 方法執行時,棧幀中局部變量表存儲的引用類型變量。
生命周期: 與方法調用綁定,方法結束后引用失效。
示例:
public void method() {Object obj = new Object(); // obj 是 GC Root// 方法執行期間,obj 引用的對象不會被回收
} // 方法結束后,obj 出棧,對象失去 GC Root 引用
2. 方法區中靜態屬性引用的對象
來源: 類的靜態變量(static 字段)。
生命周期: 與類的生命周期相同,類卸載前始終存活。
示例:
public class MyClass {static Object staticObj = new Object(); // staticObj 是 GC Root
}
3. 方法區中常量引用的對象
來源: 字符串常量池(String Table)、類常量(final static)等。
示例:
public class Constants {public static final String NAME = "Doubao"; // "Doubao" 是 GC Rootpublic static final List<String> LIST = Collections.unmodifiableList(Arrays.asList("a", "b") // 列表對象是 GC Root);
}
4. 本地方法棧中 JNI 引用的對象
來源: Java 調用本地代碼(如 C/C++)時,本地方法棧中保存的引用。
示例:
public native void nativeMethod(); // 本地方法可能持有對象引用,成為 GC Root
5. 其他特殊引用
- 活動線程(Active Threads):
Thread thread = new Thread(() -> {Object obj = new Object(); // obj 被線程棧引用,成為 GC Root// ...
});
thread.start();
- 類加載器(ClassLoader):
加載的類和靜態變量。
- JVM 內部對象:
如系統類(java.lang.Class)、異常對象(ThreadDeath)等。
GC Roots 內存示意圖
三、垃圾回收算法詳解
3.1 標記 - 清除(Mark-Sweep)
步驟:
1. 標記: 在堆中遍歷所有對象,找出內存中需要回收的對象,并且把它們標記出來。
(藍色為存活對象、灰色為可回收對象、白色為可用內存)
2. 清除: 清除掉被標記需要回收的對象,釋放出對應的內存空間。
缺點:
- 效率低: 兩次遍歷堆,耗時較長。
- 內存碎片: 回收后產生不連續內存塊,可能導致大對象分配失敗。
3.2 復制(Copying)
步驟:
- 將內存分為Eden 區和兩個Survivor 區(通常比例 8:1:1)。
- 新對象分配到 Eden 區,當 Eden 區滿時觸發 Minor GC。
- 存活對象復制到 From Survivor (s0) 區,清空 Eden 區。
- 下次 GC 時,將 Eden+ From Survivor (s0) 的存活對象復制到 To Survivor (s1) 區,清空原區域。
- 經歷多次 GC 仍存活的對象晉升到老年代。
圖解:
- 將內存劃分為兩塊相等的區域,每次只使用其中一塊
(藍色為存活對象、灰色為可回收對象、白色為可用內存、綠色為保留內存)
3. 當其中一塊內存使用完了,就將還存活的對象復制到另外一塊上面,然后把已經使用過的內存空間一次清除掉。
優點: 無內存碎片,效率高(只需移動指針)。
缺點: 浪費 50% 內存空間(實際采用 8:1:1 比例,僅浪費 10%)。
3.3 標記 - 整理(Mark-Compact)
步驟:
- 標記: 遍歷標記存活對象(灰色)。
- 整理: 將存活對象向一端移動,直接清理邊界外的內存(白色區域)。
- 優點: 無內存碎片,適合對象存活率高的場景(如老年代)。
圖解:
- 標記過程仍然與 標記-清除算法 一樣,但是后續步驟不是直接對可回收對象進行清理,
而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
(藍色為存活對象、灰色為可回收對象、白色為可用內存)
- 讓所有存活的對象都向一端移動,清理掉邊界以外的內存。
缺點: 需移動對象,成本較高。
3.4 分代收集(Generational Collection)
原理:
根據對象存活周期將堆分為新生代和老年代:
- 新生代(Eden+Survivor): 對象存活率低,采用 復制算法。
- 老年代: 對象存活率高,采用 標記 - 清除 或 標記 - 整理算法 。
- 永久代 / 元空間: 存儲類信息、常量池等,GC 頻率低。
四、垃圾收集器詳解
以下是 Java 主流垃圾回收器的核心啟動參數及典型配置
按 分代收集器 和 分區收集器 分類整理:
I、分代收集器
4.1 Serial 收集器
特點: 單線程,STW(Stop The World)【全程暫停用戶應用程序】,采用復制算法。
算法:
-
新生代:復制算法(Copying)
將存活對象從Eden區和Survivor區復制到另一個Survivor區,清除未存活對象。 -
老年代:標記-整理算法(Mark-Compact)
標記存活對象后,整理內存空間,消除碎片。
適用場景:
單線程環境(Client 模式),客戶端模式或小內存應用。
參數:
# 新生代和老年代均使用Serial(單線程)
-XX:+UseSerialGC -Xmx512m
4.2 Parallel Scavenge 收集器(吞吐量優先)
特點: 多線程,關注吞吐量(運行用戶代碼時間 / 總時間)。
算法:
-
新生代(Parallel Scavenge):復制算法
多線程并行復制存活對象,提升回收效率。 -
老年代(Parallel Old):標記-整理算法
多線程并行標記和整理,減少碎片。
適用場景:
多核服務器,注重吞吐量而非低延遲。
參數:
# 新生代Parallel Scavenge + 老年代Parallel Old
-XX:+UseParallelGC -XX:+UseParallelOldGC -Xmx4g # 控制最大停頓時間(毫秒)
-XX:MaxGCPauseMillis=100 # 控制吞吐量目標(默認99%)(如 99 表示 1% 時間用于 GC)。
-XX:GCTimeRatio=99# 自適應調整內存區域大小
-XX:+UseAdaptiveSizePolicy
4.3 CMS(Concurrent Mark Sweep)收集器(低延遲)
算法:
- 標記-清除算法(Mark-Sweep)
- 工作流程:
1. 初始標記(STW,短暫):標記 GC Roots 直接關聯的對象。
2. 并發標記:與用戶線程并發執行,遍歷可達對象。
3. 重新標記(STW):修正并發標記階段因用戶線程運行導致的對象引用變化(使用增量更新算法)。
4. 并發清除:與用戶線程并發未標記對象。
缺點:
-
內存碎片: 標記-清除算法不整理內存,長期運行后碎片化嚴重,可能導致:
-
晉升失敗(Promotion Failed): 新生代對象無法晉升到老年代。
-
大對象分配失敗(Humongous Allocation Failure)。
-
最終觸發 Full GC,使用 Serial Old(標記-整理算法) 壓縮內存。
CMS的核心目標是通過標記-清除算法減少垃圾回收的停頓時間(STW)。然而,由于其設計上的限制,CMS在某些場景下觸發Full GC,無法通過并發操作完成垃圾回收,此時JVM會觸發故障回退機制(Fallback Mechanism),強制使用單線程的Serial Old收集器(標記-整理算法)進行壓縮執行Full GC以恢復內存可用性。這種設計是CMS在低延遲與內存連續性之間的權衡。
-
-
吞吐量下降: 并發階段占用 CPU 資源,與用戶線程競爭。
-
無法處理突發分配壓力: 并發周期中若老年代空間不足,觸發并發模式失敗(Concurrent Mode Failure)。
觸發Full GC的場景 | 描述 | 是否使用Serial Old |
---|---|---|
并發模式失敗 | 并發標記期間老年代空間不足 | 是 |
晉升失敗 | 新生代對象晉升到老年代時因碎片無法分配 | 是 |
顯式調用System.gc() | 強制觸發Full GC(默認開啟ExplicitGCInvokesConcurrent可繞過此行為) | 是 |
元空間耗盡 | 類元數據區無法擴展 | 是 |
其他內存分配失敗 | 直接內存(Direct Memory)或本地內存(Native Memory)耗盡等 | 是 |
適用場景:
Web 服務器、需要快速響應的應用、老年代垃圾回收、追求低停頓時間。
參數:
# 新生代ParNew + 老年代CMS
-XX:+UseConcMarkSweepGC -Xmx8g # 老年代使用70%時觸發CMS(默認92%)
-XX:CMSInitiatingOccupancyFraction=70# 控制Full GC時是否進行內存壓縮(減少碎片)(標記-整理)。
-XX:+UseCMSCompactAtFullCollection# 并行執行重新標記階段
-XX:+CMSParallelRemarkEnabled# 指定經過多少次Full GC后執行一次壓縮(0表示每次Full GC都壓縮)。
-XX:CMSFullGCsBeforeCompaction=0
II、分區收集器
4.4 G1(Garbage-First)收集器(區域化、平衡延遲與吞吐量)
算法:
-
分區(Region)
將堆劃分為多個大小相等的 Region(如 2MB-32MB)。 -
新生代:復制算法
存活對象復制到Survivor Region或晉升到老年代Region。 -
老年代:標記-整理算法
并發標記后,優先回收垃圾最多的Region(Garbage-First),并整理Region內空間。 -
動態分代
每個 Region 可動態扮演 Eden、Survivor、Old 或 Humongous(大對象)。 -
可預測停頓
根據 Region 的回收價值(垃圾占比)排序,優先回收收益高的區域。 -
工作流程
1. 初始標記(STW 極短): 標記 GC Roots 直接關聯的 Region。
2. 并發標記: 與用戶線程并發標記存活對象。
3. 最終標記(STW): 處理并發階段的增量更新(使用 SATB 快照算法)。
4. 篩選回收(STW): 根據停頓時間目標選擇 Region 進行回收,采用復制算法。
關鍵說明:
G1 設計上通過 Mixed GC(混合回收) 避免 Full GC,但在極端情況(如并發標記失敗)下仍可能觸發 Full GC,此時 觸發故障回退機制(Fallback Mechanism) 到單線程的 Serial Old收集器(標記-整理算法)。
- 觸發條件: 堆內存不足且無法通過 Mixed GC 回收足夠空間。
- 優化建議: 通過 -XX:G1ReservePercent 增加預留內存,降低 Full GC 概率。
適用場景:
大堆內存(數十GB至數百GB)、追求低延遲與吞吐量平衡的應用。
參數:
# 啟用G1收集器(JDK 9+默認)
-XX:+UseG1GC -Xmx16g# 設置最大停頓時間目標(毫秒)
-XX:MaxGCPauseMillis=200# 設置Java堆區域大小(默認2的冪,1-32MB)
-XX:G1HeapRegionSize=8m# 老年代占用50%時啟動混合回收
-XX:InitiatingHeapOccupancyPercent=50
# G1 垃圾收集器預留堆內存的百分比,默認值:10(即預留 10% 的堆內存)
-XX:G1ReservePercent=10
4.5 ZGC收集器(Z Garbage Collector)(超低延遲,JDK 11+)
特點:
-
著色指針(Colored Pointers)
將 GC 信息(Marked0/1、Remapped)存儲在指針的低 4 位,無需掃描對象頭。 -
讀屏障(Load Barrier)
在讀取對象引用時動態修正指針,實現并發移動。 -
并發整理
全程幾乎無 STW(僅初始標記和再標記有極短停頓,通常 < 1ms)。
適用場景:
超大堆內存(TB 級),追求極致低延遲(<10ms),如金融交易系統。
算法:
-
并發標記-整理算法(Concurrent Mark-Compact)
1. 并發標記: 利用染色指針(Colored Pointers)標記對象狀態。
2. 并發預備重分配: 確定需要清理的內存區域。
3. 并發重分配: 將存活對象復制到新地址,并更新引用。
4. 并發重映射: 修復舊地址到新地址的映射。
5. 關鍵技術: 染色指針和讀屏障,支持全階段并發執行。
參數:
# 啟用ZGC(需JDK 11+)
-XX:+UseZGC -Xmx32g # 設置最大堆大小(ZGC適合大內存)
-Xmx64g -Xms64g# 設置并發GC線程數(默認CPU核心數的1/4)
-XX:ConcGCThreads=4# 配置GC周期的最大停頓時間(目標值)
-XX:MaxGCPauseMillis=10
4.6 Shenandoah收集器(超低延遲,OpenJDK 12+)
特點:
- 與 ZGC 類似,基于轉發指針和布魯姆過濾器實現并發回收,停頓時間極短。
適用場景:
OpenJDK 環境下的超大堆內存(TB級)、極低停頓時間(亞毫秒級)、低延遲應用。
算法:
-
并發復制算法(Concurrent Copying)
1. 并發標記: 與用戶線程并發標記存活對象。
2. 并發預清理: 確定需要回收的Region。
3. 并發轉移: 在用戶線程運行時,將存活對象復制到新Region。
4. 引用更新: 并發更新對象引用至新地址。
5. 關鍵技術: 使用 讀屏障(Read Barrier) 實現并發對象移動。
參數:
# 啟用Shenandoah(需JDK 12+,OpenJDK默認包含)
-XX:+UseShenandoahGC -Xmx16g # 設置GC策略(normal/size/reference-time)
-XX:ShenandoahGCHeuristics=normal# 設置最大停頓時間目標(毫秒)
-XX:MaxGCPauseMillis=100
III、垃圾收集器對比表
收集器 | 新生代算法 | 老年代算法 | Full GC 收集器 | JDK版本支持 | 默認收集器(JDK版本) | 推薦堆大小 | 吞吐量 | 適用場景 | 是否開源 | 所屬JVM實現 | 垃圾對象判定算法 | 核心特點 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Serial | 復制 | 標記-整理 | Serial Old(標記-整理) | JDK 1.0+ | JDK 1.0~8(客戶端模式) | <512MB | 低 | 客戶端應用、嵌入式系統 | 是 | HotSpot | 可達性分析(迭代標記) | 單線程簡單高效,無多線程開銷 |
Parallel | 復制 | 標記-整理 | Parallel Old(標記-整理) | JDK 1.4+ | JDK 8(服務器模式) | 512MB~32GB | 高 | 批處理、計算密集型任務 | 是 | HotSpot | 可達性分析(并行標記) | 多線程并行回收,最大化吞吐量 |
CMS | 復制(ParNew) | 標記-清除 | Serial Old(標記-整理) | JDK 1.5~JDK14 (JDK 9+ 標記為廢棄,JDK 14+ 移除) | 無(需手動啟用) | 512MB~4GB | 中 | 低延遲Web服務(中小堆) | 是 | HotSpot | 可達性分析(三色標記 + 增量更新) | 低延遲,并發標記減少停頓,但需監控碎片問題 |
G1 | 復制 | 標記-整理(分Region) | Serial Old(標記-整理) | JDK 7u4+ | JDK 9+(服務器模式) | 4GB~64GB | 中~高 | 大堆內存、平衡型應用 | 是 | HotSpot | 可達性分析(三色標記 + SATB快照) | 分區回收,可預測停頓,自動整理碎片 |
ZGC | 并發復制 | 并發標記-整理 | 無(無 Full GC 概念) | OpenJDK 11+ | JDK 15+(可選) | ≥4GB | 中 | 超大堆、亞毫秒級停頓 | 是 | HotSpot | 可達性分析(并發三色標記 + 染色指針) | 超大堆,染色指針,停頓<1ms |
Shenandoah | 并發復制 | 并發復制 | 無(無 Full GC 概念) | OpenJDK 12+ | 無(需手動啟用) | ≥4GB | 中 | 超大堆、極低延遲需求 | 是 | HotSpot | 可達性分析(并發三色標記 + 讀屏障) | 全堆并發,停頓<10ms |
IIII、垃圾對象判定算法詳解
收集器 | 判定算法實現 |
---|---|
Serial | 遞歸遍歷GC Roots,標記可達對象(無并發優化)。 |
Parallel | 多線程并行遍歷GC Roots,標記可達對象。 |
CMS | 三色標記法 + 增量更新(將修改引用的黑色對象重新標記為灰色)。 |
G1 | 三色標記法 + SATB(原始快照)(基于標記開始時的引用關系進行標記)。 |
ZGC | 并發三色標記法 + 染色指針(通過指針元數據記錄對象狀態)。 |
Shenandoah | 并發三色標記法 + 讀屏障(攔截對象訪問,處理并發標記中的引用變化)。 |
五、GC 日志分析與可視化工具
5.1 典型 GC 日志示例
[GC (Allocation Failure) [PSYoungGen: 524288K->1024K(524288K)] 524288K->1025K(1048576K), 0.0012345 secs]
解析:
- GC:新生代 GC。
- Full GC:整堆 GC。
- Allocation Failure:因內存分配失敗觸發 GC。
- PSYoungGen:Parallel Scavenge 收集器的新生代。
- 524288K->1024K:回收前→回收后的內存使用量。
- 0.0012345 secs:GC 耗時。
5.2 可視化工具推薦
- GCViewer:解析 GC 日志,生成可視化圖表(如停頓時間、內存使用趨勢)。
- GCEasy:在線工具,上傳 GC 日志后提供詳細分析報告和優化建議。
- JProfiler:集成 GC 分析功能,支持實時監控和堆轉儲分析。
- Java Mission Control (JMC):Oracle 官方工具,提供高性能監控和診斷。
- Arthas(阿爾薩斯):阿里巴巴開源的 實時診斷工具,通過命令行或 Web UI 動態監控和排查問題。
- VisualVM:輕量級可視化工具,集成多種性能分析功能(含 GC 監控),支持插件擴展。
六、性能調優實戰案例
6.1 高并發 Web 應用調優
場景: 8 核 16GB 服務器,Tomcat 應用,響應時間敏感。
配置:
java -Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=8m \
-XX:+ParallelRefProcEnabled \
-jar app.jar
效果:
- 新生代 GC 頻率降低,停頓時間控制在 50ms 以內。
- 老年代碎片化問題緩解,Full GC 頻率從每天數次降為每周一次。
6.2 大數據批處理調優
場景: Spark 作業,64GB 內存,追求高吞吐量。
配置:
spark-submit --conf spark.executor.memory=48g \
--conf spark.executor.extraJavaOptions="-XX:+UseParallelGC -XX:GCTimeRatio=99" \
--class com.example.BatchJob app.jar
效果:
- 吞吐量提升 20%,GC 時間占比從 5% 降至 1%。
- 并行 GC 充分利用多核 CPU,減少 STW 時間。
七、總結與最佳實踐
垃圾判定:優先使用可達性分析,避免引用計數法的循環引用問題。
算法選擇:
- 新生代:對象存活率低→復制算法。
- 老年代:對象存活率高→標記 - 整理算法。
收集器選型:
- 小內存 / 單線程:Serial。
- 吞吐量優先:Parallel。
- 低延遲:CMS(老年代)、G1(大內存)、ZGC/Shenandoah(超大內存)。
性能監控:
- 定期分析 GC 日志,監控停頓時間和內存使用趨勢。
- 使用可視化工具定位內存泄漏和大對象問題。
通過結合圖解和實戰案例,可更直觀地理解 JVM 垃圾回收機制的底層原理,從而針對性地優化應用性能。
附:
- JVM圖解
- ??運行時數據區的初始化時機
運行時數據區 | 初始化時機 | 與類加載的關系 |
---|---|---|
方法區 | JVM 啟動時分配內存,類加載時寫入類元信息、常量池、靜態變量等。 | 類加載的初始化階段會向方法區寫入靜態變量。 |
堆 | JVM 啟動時分配內存,對象實例在 new 時創建。 | 類初始化完成后,通過 new 觸發對象分配。 |
虛擬機棧 | 線程創建時初始化,每個線程有獨立棧空間,棧幀在方法調用時動態生成。 | 與類加載無關,由線程執行引擎管理。 |
程序計數器 | 線程創建時初始化,記錄當前執行指令地址。 | 與類加載無關,由線程執行引擎管理。 |
本地方法棧 | 線程創建時初始化,存儲本地方法(Native Method)調用信息。 | 與類加載無關,由本地方法接口(JNI)管理。 |
- 類加載與運行時數據區的關系