【Java 底層】JVM 垃圾回收機制深度剖析:從對象生死判定到收集器實戰
【Java 底層】JVM 垃圾回收機制深度剖析:從對象生死判定到收集器實戰
Java 之所以被稱為 “開發效率利器”,很大程度上得益于其自動內存管理機制 —— 開發者無需手動分配和釋放內存,一切由 JVM 垃圾回收器(Garbage Collector)自動完成。但 “自動” 不代表 “無感知”:內存泄漏、OOM 異常、GC 頻繁卡頓等問題,本質上都是對垃圾回收機制理解不深導致的。
本文將從 “如何判斷對象該回收”“用什么算法回收”“不同場景選哪種回收器” 三個維度,深入拆解 JVM 垃圾回收的底層邏輯,幫你從根源上解決內存相關問題。
一、前置問題:JVM 為什么需要垃圾回收?
在 C/C++ 中,開發者需要手動調用 malloc()
分配內存、free()
釋放內存,一旦遺漏 free()
就會導致內存泄漏(無用內存占用不釋放),最終可能引發內存溢出(OOM)。
Java 通過垃圾回收器解決了這個問題:
-
自動識別 “不再被使用的對象”(垃圾);
-
自動釋放這些對象占用的內存;
-
優化內存碎片,提高內存利用率。
但垃圾回收并非 “免費午餐”—— 回收過程會暫停應用線程(STW,Stop-The-World),頻繁或過長的 STW 會導致應用卡頓。因此,垃圾回收的核心目標是:在盡可能短的 STW 時間內,高效回收無用內存。
二、第一步:如何判定對象 “已死”?兩種核心算法的博弈
垃圾回收的前提是 “識別垃圾”。JVM 采用兩種主流算法判斷對象是否存活:引用計數法和可達性分析。
1. 引用計數法:簡單但 “有漏洞” 的方案
原理:給每個對象設置一個 “引用計數器”,當有地方引用該對象時,計數器 + 1;引用失效時,計數器 - 1。當計數器為 0 時,認為對象可回收。
優點:實現簡單,判斷效率高。
缺點:無法解決 “循環引用” 問題。例如:
class A {B b;
}
class B {A a;
}public static void main(String[] args) {A a = new A();B b = new B();a.b = b; // A 引用 Bb.a = a; // B 引用 Aa = null; // 取消外部對 A 的引用b = null; // 取消外部對 B 的引用
}
此時 A 和 B 的引用計數器都是 1(互相引用),但已無外部引用,理應被回收,可引用計數法會誤判為 “存活”,導致內存泄漏。
因此,JVM 未采用引用計數法,而是選擇了更可靠的可達性分析。
2. 可達性分析法:JVM 的 “標準方案”
原理:以 “GC Roots” 為起點,向下搜索引用鏈(Reference Chain)。如果一個對象到 GC Roots 沒有任何引用鏈相連(即不可達),則認為該對象可回收。
GC Roots 包含哪些對象?
-
虛擬機棧中引用的對象(如局部變量、方法參數);
-
方法區中類靜態屬性引用的對象(如
static
變量); -
方法區中常量引用的對象(如
final
變量); -
本地方法棧中 JNI(Native 方法)引用的對象。
示例:上述 A 和 B 的循環引用案例中,a 和 b 被置為 null 后,A 和 B 到 GC Roots 均無引用鏈,因此會被判定為可回收,解決了循環引用問題。
3. 補充:引用的 “強度分級”
Java 中的 “引用” 并非非黑即白。JDK 1.2 后,引用被分為四級,強度從強到弱依次為:
-
強引用:普通引用(如
Object obj = new Object()
),只要強引用存在,對象永遠不會被回收; -
軟引用:
SoftReference
包裝,內存不足時才會被回收(適合緩存場景); -
弱引用:
WeakReference
包裝,下次 GC 時必定被回收(適合臨時數據); -
虛引用:
PhantomReference
包裝,唯一作用是在對象被回收時收到通知(幾乎不用)。
這四種引用讓垃圾回收更靈活 —— 例如,緩存數據可用軟引用,既保證內存不足時自動釋放,又能在有內存時保留緩存。
三、第二步:如何回收?三大核心算法與內存整理
判定對象 “已死” 后,需要回收其占用的內存。JVM 采用三種經典回收算法,各有適用場景。
1. 標記 - 清除算法(Mark-Sweep):最基礎但 “有瑕疵”
步驟:
-
標記:通過可達性分析,標記所有可回收的對象;
-
清除:遍歷內存,回收所有被標記的對象,釋放內存空間。
優點:實現簡單,不需要移動對象。
缺點:
-
產生內存碎片:回收后內存中會出現大量不連續的空閑區域,當需要分配大對象時,可能因找不到足夠大的連續空間而觸發新的 GC;
-
效率較低:標記和清除過程都需要遍歷大量對象。
2. 復制算法(Copying):犧牲空間換效率
步驟:
-
將內存分為大小相等的兩塊(From 區和 To 區);
-
只使用 From 區,To 區空閑;
-
GC 時,將 From 區中存活的對象復制到 To 區(按順序排列,無碎片);
-
清空 From 區,交換 From 和 To 區的角色,重復使用。
優點:
-
無內存碎片;
-
回收效率高(只需復制存活對象,存活對象少的時候效率極高)。
缺點:
-
內存利用率低(僅能使用一半內存);
-
不適合存活對象多的場景(如老年代,復制成本太高)。
應用:JVM 年輕代(Eden 區和 Survivor 區)主要采用復制算法。年輕代中對象存活率低,復制成本小,且通過 “Eden + 2 個 Survivor 區(8:1:1)” 的設計,將內存浪費控制在 10%(只留 1 個 Survivor 區空閑)。
3. 標記 - 整理算法(Mark-Compact):老年代的 “專屬方案”
步驟:
-
標記:同標記 - 清除算法,標記可回收對象;
-
整理:將所有存活對象向內存一端移動,然后直接清理掉邊界外的內存。
優點:
-
無內存碎片;
-
內存利用率 100%(無需犧牲一半空間)。
缺點:
- 增加了 “移動對象” 的成本,效率比復制算法低。
應用:JVM 老年代主要采用標記 - 整理算法。老年代中對象存活率高,移動成本雖高,但避免了內存碎片和空間浪費,是更平衡的選擇。
四、第三步:誰來執行回收?經典垃圾收集器的 “看家本領”
垃圾收集器是算法的具體實現。JVM 提供了多種收集器,各有側重,需根據應用場景選擇。
1. Serial GC:單線程回收,簡單高效但卡頓明顯
特點:
-
單線程執行 GC,GC 時暫停所有應用線程(STW);
-
采用 “復制算法(年輕代)+ 標記 - 整理算法(老年代)”;
-
實現簡單,內存占用少,適合單核 CPU 或小內存應用(如嵌入式設備)。
缺點:STW 時間長(尤其老年代回收時),不適合大內存、高并發場景。
2. Parallel GC:多線程 “吞吐量優先”
特點:
-
多線程執行 GC(年輕代和老年代均用多線程),縮短 STW 時間;
-
目標是 “高吞吐量”(吞吐量 = 運行用戶代碼時間 / 總時間);
-
可通過參數控制吞吐量(如
-XX:MaxGCPauseMillis
限制最大 STW 時間,-XX:GCTimeRatio
控制 GC 時間占比)。
應用:適合后臺任務、批處理程序等對吞吐量敏感,對延遲要求不高的場景。
3. CMS GC:“低延遲” 的并發回收器(已逐漸被淘汰)
CMS(Concurrent Mark Sweep) 是第一款真正意義上的并發收集器,目標是 “最短 STW 時間”。
步驟(分四階段):
-
初始標記(STW):快速標記 GC Roots 直接關聯的對象(耗時短);
-
并發標記:GC 線程與應用線程并發執行,遍歷引用鏈,標記所有可達對象(耗時最長,但不阻塞應用);
-
重新標記(STW):修正并發標記期間因應用線程運行導致的標記變動(耗時較短);
-
并發清除:GC 線程與應用線程并發執行,回收被標記的對象(不阻塞應用)。
優點:STW 時間短,適合對延遲敏感的應用(如 Web 服務)。
缺點:
-
并發階段占用 CPU 資源,可能導致應用響應變慢;
-
采用標記 - 清除算法,會產生內存碎片;
-
對大內存支持不好(并發標記時內存占用高);
-
JDK 9 中被標記為 deprecated,JDK 14 中移除。
4. G1 GC:面向大內存的 “區域化” 收集器
G1(Garbage-First)是為替代 CMS 設計的,適合 4GB 以上大內存場景,兼顧吞吐量和延遲。
核心創新:
-
區域化內存布局:將堆內存劃分為多個大小相等的獨立區域(Region),每個區域可動態扮演年輕代或老年代,靈活分配內存;
-
Mixed GC:優先回收 “垃圾最多的區域”(Garbage-First),提高回收效率;
-
停頓預測模型:根據歷史數據預測 STW 時間,確保不超過用戶設置的目標(如
-XX:MaxGCPauseMillis=200
)。
步驟:類似 CMS,但增加了 “篩選回收” 階段,只回收垃圾多的區域,減少 STW 時間。
優點:
-
大內存下表現優異,STW 時間可控;
-
無內存碎片(區域內采用復制算法,整體類似標記 - 整理);
-
兼顧吞吐量和延遲,是當前主流收集器之一。
5. ZGC/Shenandoah:超低延遲的新一代收集器
JDK 11 引入 ZGC,JDK 12 引入 Shenandoah,二者均為 “低延遲、高并發” 收集器,STW 時間可控制在毫秒級甚至微秒級,適合超大內存(TB 級)場景。
核心技術:
-
著色指針:通過指針標記對象狀態(如是否被標記、是否可回收),避免傳統的 “標記 - 清除” 流程;
-
讀屏障 / 寫屏障:在對象引用讀寫時插入少量代碼,實現并發標記和移動,幾乎不阻塞應用線程。
應用:對延遲要求極高的場景(如高頻交易、實時數據分析),但目前在生產環境中的應用不如 G1 廣泛。
五、實戰避坑:GC 問題的排查與優化思路
1. 常見問題及原因:
-
頻繁 Full GC:可能是內存泄漏(對象長期被引用無法回收)、老年代對象增長過快(如大對象直接進入老年代);
-
STW 時間過長:收集器選擇不當(如用 Serial GC 處理大內存)、堆內存設置不合理(太大或太小);
-
OOM 異常:堆內存不足(需調大
-Xmx
)、永久代 / 元空間不足(調大-XX:MaxMetaspaceSize
)。
2. 優化原則:
-
根據場景選收集器:延遲敏感用 G1/ZGC,吞吐量優先用 Parallel GC;
-
合理設置堆內存:避免太小(GC 頻繁)或太大(單次 GC 時間長),通常建議為物理內存的 1/2 ~ 1/4;
-
減少大對象:大對象直接進入老年代,易觸發 Full GC,盡量拆分或復用對象;
-
監控與調優:通過
jstat
、jconsole
或可視化工具(如 GCEasy)監控 GC 頻率和 STW 時間,逐步調整參數。
六、總結:垃圾回收的 “本質” 是平衡的藝術
JVM 垃圾回收的核心是 “在效率、延遲、內存利用率之間找平衡”:
-
年輕代用復制算法,優先效率;
-
老年代用標記 - 整理算法,優先避免碎片;
-
收集器選擇則根據 “吞吐量” 或 “延遲” 需求,從 Serial 到 ZGC,本質是對 “并發” 和 “STW” 的權衡。
理解這些底層邏輯,不僅能解決 GC 相關問題,更能幫你寫出更 “內存友好” 的代碼 —— 比如避免創建不必要的對象、及時釋放無用引用、合理設計緩存策略等。畢竟,最好的 GC 優化,是從代碼層面減少垃圾的產生。