Java 虛擬機(JVM)的垃圾回收(Garbage Collection,GC)機制是自動管理內存的核心,其核心目標是識別并回收不再被使用的對象所占用的內存,避免內存泄漏和溢出。以下從垃圾判斷方法、垃圾回收算法和具體垃圾收集器三個層面詳細說明:
一、垃圾判斷方法:如何識別 "垃圾" 對象
垃圾回收的前提是判斷哪些對象已經不再被使用(即 "垃圾")。JVM 主要采用兩種判斷方式:
1. 引用計數法(Reference Counting)
- 原理:每個對象維護一個 "引用計數器",當對象被引用時計數器 + 1,引用失效時 - 1;當計數器為 0 時,認為對象是垃圾。
- 優點:實現簡單,判斷效率高。
- 缺點:無法解決循環引用問題(如 A 引用 B,B 引用 A,兩者計數器均不為 0,但實際已無外部引用)。
- 現狀:Java 虛擬機未采用這種方式(因循環引用問題),Python 等語言使用。
2. 可達性分析算法(Reachability Analysis)
- 原理:以 "GC Roots" 為起點,通過引用鏈遍歷對象;若對象無法通過任何引用鏈連接到 GC Roots,則被判定為垃圾。
- GC Roots:指一系列 "根對象",包括:
- 虛擬機棧(棧幀中的局部變量表)中引用的對象;
- 方法區中類靜態屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中 JNI(Native 方法)引用的對象。
- 優點:解決了循環引用問題,是 Java 虛擬機的核心判斷方式。
- 擴展:Java 中的 "引用" 被細分為 4 種類型(強引用、軟引用、弱引用、虛引用),不同引用類型影響對象被回收的時機(如軟引用在內存不足時才會被回收)。
二、垃圾回收算法:如何回收垃圾
確定垃圾對象后,需要通過具體算法回收其內存。常見的基礎算法包括:
1. 標記 - 清除算法(Mark-Sweep)
- 步驟:
- 標記:通過可達性分析標記所有存活對象(非垃圾);
- 清除:遍歷堆內存,回收所有未被標記的對象(垃圾)。
- 優點:實現簡單,無需移動對象。
- 缺點:
- 效率低:標記和清除過程都需要遍歷整個堆,耗時較長;
- 內存碎片:回收后會產生大量不連續的內存碎片,可能導致大對象無法分配內存。
2. 標記 - 復制算法(Mark-Copy)
- 步驟:
- 將堆內存分為大小相等的兩塊(如 A 和 B),僅使用 A 塊;
- 標記:標記 A 塊中的存活對象;
- 復制:將 A 塊中所有存活對象復制到 B 塊(按順序連續放置);
- 清除:清空 A 塊,后續內存分配僅使用 B 塊(下次回收時交換角色)。
- 優點:
- 效率高:復制存活對象的成本低于清除大量垃圾;
- 無內存碎片:存活對象連續放置,內存分配簡單(指針碰撞即可)。
- 缺點:
- 內存利用率低:僅能使用一半內存;
- 不適合存活對象多的場景(復制成本高)。
- 適用場景:Java 新生代(因新生代對象存活時間短,存活對象少)。
3. 標記 - 整理算法(Mark-Compact)
- 步驟:
- 標記:標記所有存活對象;
- 整理:將所有存活對象向內存一端移動,按順序排列;
- 清除:直接清除邊界外的所有內存(垃圾)。
- 優點:解決了標記 - 清除的內存碎片問題,且內存利用率 100%。
- 缺點:整理階段需要移動對象,成本較高(尤其是老年代對象存活時間長,移動成本大)。
- 適用場景:Java 老年代(因老年代對象存活時間長,存活對象多,需避免碎片)。
4. 分代收集算法(Generational Collection)
- 原理:根據對象存活周期將堆內存分為新生代和老年代,針對不同區域采用不同算法(結合上述基礎算法的優勢):
- 新生代:對象存活時間短(朝生夕死),適合標記 - 復制算法(只需復制少量存活對象);
- 老年代:對象存活時間長(存活概率高),適合標記 - 清除或標記 - 整理算法(避免頻繁移動對象)。
- 細節:新生代進一步分為 Eden 區(80%)和兩個 Survivor 區(From、To 各 10%),分配對象時先在 Eden 區,回收時將存活對象復制到 Survivor 區,多次存活后進入老年代。
- 現狀:幾乎所有 Java 虛擬機都采用分代收集算法作為基礎框架。
三、垃圾收集器:算法的具體實現
垃圾收集器是垃圾回收算法的具體實現,不同收集器針對不同場景(如吞吐量、延遲)優化。Java 虛擬機中常見的收集器包括:
1. Serial GC(串行收集器)
- 特點:單線程執行垃圾回收,回收時暫停所有用戶線程("Stop The World",STW)。
- 算法:
- 新生代:標記 - 復制;
- 老年代:標記 - 整理。
- 優點:實現簡單,內存占用少,適合單核 CPU 環境。
- 缺點:STW 時間長,不適合多線程、大堆內存應用。
- 適用場景:客戶端應用(如桌面程序),JVM 默認客戶端模式下的收集器。
2. ParNew GC(并行新生代收集器)
- 特點:Serial GC 的多線程版本,僅作用于新生代,老年代仍需配合 Serial Old 或 CMS。
- 算法:新生代采用標記 - 復制(多線程并行標記和復制)。
- 優點:利用多 CPU 加速新生代回收,減少 STW 時間。
- 缺點:仍有 STW,老年代若配合 Serial Old 會導致長停頓。
- 適用場景:多 CPU 環境下的服務端應用,常作為 CMS 收集器的新生代搭檔。
3. Parallel Scavenge GC(并行清除收集器)
- 特點:注重吞吐量(吞吐量 = 用戶代碼執行時間 /(用戶代碼時間 + GC 時間)),屬于 "吞吐量優先" 收集器。
- 算法:
- 新生代:標記 - 復制(多線程并行);
- 老年代:Parallel Old(標記 - 整理,多線程并行)。
- 優點:可自動調節 GC 參數(如新生代大小、晉升老年代閾值)以追求最高吞吐量。
- 缺點:STW 時間可能較長,不適合對延遲敏感的應用。
- 適用場景:后臺計算(如數據分析)等對吞吐量要求高、可接受一定停頓的場景。
4. CMS(Concurrent Mark Sweep,并發標記清除)
- 特點:以低延遲為目標,盡可能減少 STW 時間,老年代收集器(需配合 ParNew 作為新生代收集器)。
- 步驟(核心是 "并發",即 GC 線程與用戶線程同時執行):
- 初始標記:標記 GC Roots 直接關聯的對象(STW,時間短);
- 并發標記:從初始標記的對象出發,遍歷引用鏈(與用戶線程并發,無 STW);
- 重新標記:修正并發標記期間因用戶線程操作導致的引用變化(STW,時間較短);
- 并發清除:回收所有未標記的對象(與用戶線程并發,無 STW)。
- 優點:并發收集,STW 時間短,適合對延遲敏感的應用(如 Web 服務)。
- 缺點:
- CPU 敏感:并發階段會占用 CPU 資源,影響用戶線程;
- 內存碎片:基于標記 - 清除算法,老年代易產生碎片;
- 需預留內存:并發清除時用戶線程仍在分配內存,需保證內存不耗盡。
- 現狀:JDK 9 中被標記為 deprecated,JDK 14 中移除,被 G1 等收集器替代。
5. G1(Garbage-First)
- 特點:區域化分代式收集器,兼顧吞吐量和延遲,適用于大堆內存(如 4GB 以上)。
- 內存布局:將堆分為多個大小相等的 Region(1MB~32MB),每個 Region 可動態標記為新生代(Eden/Survivor)或老年代,無需物理隔離。
- 核心思想:優先回收 "垃圾最多的 Region"(Garbage-First),減少 GC 時間。
- 步驟:
- 初始標記:標記 GC Roots 直接關聯的對象(STW);
- 并發標記:遍歷引用鏈,計算每個 Region 的垃圾占比(與用戶線程并發);
- 最終標記:修正并發標記的偏差(STW,使用 SATB 算法高效處理);
- 篩選回收:根據 Region 的垃圾占比排序,優先回收垃圾多的 Region(多線程并行,STW,采用標記 - 復制算法避免碎片)。
- 優點:
- 靈活處理大堆內存,延遲可控(可設置最大 STW 時間);
- 無內存碎片(篩選回收時采用復制算法)。
- 適用場景:JDK 9 及以上默認收集器,適合中大型應用(如服務器、云原生應用)。
6. 低延遲收集器(ZGC、Shenandoah)
ZGC(JDK 11 引入):
- 目標:STW 時間不超過 10ms,支持 TB 級堆內存。
- 特點:基于 Region,采用 "著色指針" 和 "讀屏障" 技術,幾乎全程并發(僅初始標記和最終標記有極短 STW)。
Shenandoah(OpenJDK 引入,非 Oracle JDK 默認):
- 目標:低延遲,支持大堆。
- 特點:采用 "并發整理" 算法,在并發階段移動對象(通過轉發指針和寫屏障實現),幾乎無 STW。
適用場景:對延遲要求極高的應用(如高頻交易、實時數據處理)。
總結
Java 虛擬機的垃圾回收機制是一個 "判斷垃圾 - 選擇算法 - 具體實現" 的完整體系:
- 通過可達性分析判斷垃圾對象;
- 基于分代思想,結合標記 - 清除、復制、整理等基礎算法;
- 不同垃圾收集器(如 Serial、CMS、G1、ZGC)針對吞吐量、延遲等不同目標優化,需根據應用場景選擇。
實際開發中,需通過監控工具(如 JConsole、VisualVM)分析 GC 日志,選擇合適的收集器并調優參數(如堆大小、新生代比例),以平衡性能需求。