內存回收機制是Java區別于C/C++等語言的核心特性之一,也是Java開發者理解程序性能、解決內存相關問題(如內存泄漏、OOM)的關鍵。
核心目標: 自動回收程序中不再使用的對象所占用的內存,防止內存耗盡,同時盡量減少對程序執行的影響(特別是停頓時間)。
一、 基礎概念與內存模型
- 自動內存管理: Java開發者無需(也無法)像C/C++那樣顯式調用
delete
或free
來釋放對象內存。JVM負責跟蹤所有對象,并在它們“不再被需要”時自動回收其占用的內存。 - 內存區域劃分 (JVM Runtime Data Areas):
- 堆 (Heap): GC工作的主戰場。存放所有對象實例和數組。這是GC管理的主要區域。堆是線程共享的。
- 方法區 (Method Area / Metaspace): 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。在Java 8及以后,永久代(PermGen)被移除,取而代之的是元空間(Metaspace),它主要使用本地內存(Native Memory)來存儲這些數據,由JVM自行管理其內存回收(主要回收不再使用的類加載器和類信息)。
- 虛擬機棧 (VM Stack): 線程私有。存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法調用創建一個棧幀。棧幀隨方法結束而銷毀,其中基本類型變量和對象引用所占內存自動釋放,不涉及GC。棧上分配的對象(逃逸分析優化)理論上也不涉及GC,但主流HotSpot JVM主要采用標量替換優化,對象本身并不在棧上連續分配。
- 本地方法棧 (Native Method Stack): 為Native方法服務,類似虛擬機棧。
- 程序計數器 (Program Counter Register): 線程私有。指示當前線程執行的字節碼指令地址。不涉及GC。
- 直接內存 (Direct Memory): 不是JVM運行時數據區的一部分,但可以通過
ByteBuffer.allocateDirect()
申請。這部分內存由操作系統管理,但Java的GC機制可以間接管理其關聯的DirectByteBuffer
對象。當DirectByteBuffer
對象被回收時,其關聯的清理器(Cleaner
)會被觸發(通過ReferenceQueue
),嘗試釋放對應的直接內存。但這依賴于GC和Cleaner
線程的執行。
二、 關鍵概念:對象存活性判斷 - “垃圾”的定義
GC要回收的是“垃圾”,即不再被任何地方引用的對象。判斷對象是否存活的核心算法:
-
引用計數法 (Reference Counting):
- 原理: 每個對象維護一個計數器,記錄有多少引用指向它。當引用被創建(如賦值)時計數器+1;當引用失效(如變量離開作用域、被置為null)時計數器-1。計數器為0的對象即視為“垃圾”。
- 優點: 實現簡單,判定效率高。
- 缺點: 無法解決循環引用問題 (A引用B,B引用A,但A和B都不再被外部引用,它們的計數器都不為0,卻永遠無法被回收)。因此,主流的Java虛擬機都不采用引用計數法作為主要的垃圾判定算法。
-
可達性分析算法 (Reachability Analysis):
- 原理: 定義一系列稱為**“GC Roots”** 的對象作為起始點集。從這些根節點開始,根據引用關系向下搜索(圖遍歷)。搜索走過的路徑稱為**“引用鏈”**。如果一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots開始不可達),則證明此對象不再被使用,可以被回收。
- GC Roots 通常包括:
- 虛擬機棧(棧幀中的局部變量表)中引用的對象。
- 本地方法棧中JNI(即Native方法)引用的對象。
- 方法區中類靜態屬性引用的對象(static變量)。
- 方法區中常量引用的對象(如字符串常量池里的引用)。
- Java虛擬機內部的引用(如基本數據類型對應的Class對象,常駐的異常對象
NullPointerException
、OutOfMemoryError
等,系統類加載器)。 - 所有被同步鎖(
synchronized
關鍵字)持有的對象。 - 反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
- 優點: 解決了循環引用問題。
- 缺點: 分析過程相對耗時(需要暫停用戶線程 - Stop-The-World, STW)。
三、 引用類型與回收強度
Java將引用分為四類,影響GC行為:
- 強引用 (Strong Reference): 最常見的引用類型(
Object obj = new Object();
)。只要強引用存在,對象就永遠不會被回收。 - 軟引用 (Soft Reference): (
java.lang.ref.SoftReference
) 在內存不足即將發生OOM之前,這些對象會被回收。 常用于實現內存敏感的緩存(如圖片緩存)。回收發生在OutOfMemoryError
拋出之前。 - 弱引用 (Weak Reference): (
java.lang.ref.WeakReference
) 下一次GC發生時,無論當前內存是否充足,都會被回收。 常用于實現規范化映射(如WeakHashMap
)或監聽器列表,防止因監聽器未注銷導致的內存泄漏。 - 虛引用 (Phantom Reference): (
java.lang.ref.PhantomReference
) 最弱的引用。無法通過虛引用獲取對象實例。對象被回收時,虛引用會被放入關聯的ReferenceQueue
。 主要用于在對象被回收時收到一個系統通知(通過檢查ReferenceQueue
),用于執行一些資源清理工作,例如DirectByteBuffer
的清理器。
四、 垃圾收集算法 - 方法論
-
標記-清除算法 (Mark-Sweep):
- 過程:
- 標記 (Mark): 從GC Roots開始,遍歷所有可達對象,進行標記(通常是在對象頭中置位)。
- 清除 (Sweep): 遍歷整個堆,回收所有未被標記的對象所占用的空間。
- 優點: 簡單直接。
- 缺點:
- 效率問題: 標記和清除兩個過程的效率都不高(遍歷整個堆)。
- 空間問題: 回收后會產生大量不連續的內存碎片。碎片過多可能導致后續分配較大對象時找不到足夠的連續內存,從而觸發另一次GC。
- 過程:
-
復制算法 (Copying):
- 過程: 將可用內存按容量劃分為大小相等的兩塊(A區和B區)。每次只使用其中一塊(如A區)。當A區滿了,觸發GC:將A區中所有存活的對象復制到B區,然后一次性清理掉整個A區。下次使用B區,如此往復。
- 優點:
- 效率高: 只需移動存活對象(通常存活對象少),按順序分配內存,沒有碎片。
- 實現簡單: 清除就是清空整個半區。
- 缺點:
- 內存利用率低: 總有一半內存空閑,浪費空間。
- 存活對象多時效率下降: 復制大量對象開銷大。
- 應用: 非常適合對象朝生夕死(存活率低)的新生代。HotSpot JVM的新生代(Eden + S0/S1)就是基于復制算法優化(Appel式回收)。
-
標記-整理算法 (Mark-Compact):
- 過程:
- 標記 (Mark): 同標記-清除。
- 整理 (Compact): 將所有存活的對象向內存空間的一端移動(滑動),然后直接清理掉邊界以外的所有內存。
- 優點:
- 沒有內存碎片: 移動后空間連續。
- 內存利用率高: 不像復制算法浪費一半空間。
- 缺點:
- 效率相對較低: 移動存活對象需要更新引用地址(需要STW),且移動本身耗時。
- 應用: 適合存活對象較多的老年代(Tenured/Old Generation)。
- 過程:
-
分代收集算法 (Generational Collection): 現代商用JVM的主流算法!
- 核心思想: 基于對象生命周期的不同,將堆內存劃分為不同的代(Generation),對不同代采用最適合的收集算法。
- 堆內存劃分:
- 新生代 (Young Generation): 存放新創建的對象。特點:絕大多數對象在這里快速死去(生命周期短)。劃分為:
- Eden區: 新對象誕生地。
- Survivor區 (S0/S1, From/To): 兩個大小相等的區域,用于保存經過一次Minor GC后存活的對象。
- 老年代 (Old/Tenured Generation): 存放在新生代中經歷多次GC后仍然存活的對象(生命周期長)。也存放大對象(超過
-XX:PretenureSizeThreshold
設置值,直接進入老年代)。 - 永久代/元空間 (PermGen/Metaspace): (如前所述,Java 8+為Metaspace) 存放類元數據、常量池等。其GC行為獨立于堆。
- 新生代 (Young Generation): 存放新創建的對象。特點:絕大多數對象在這里快速死去(生命周期短)。劃分為:
- GC類型:
- Minor GC / Young GC: 只回收新生代的垃圾。觸發頻繁,速度快(因為新生代小且對象存活率低,通常采用優化的復制算法)。
- Major GC / Full GC: 回收整個堆(包括新生代、老年代)以及(通常)方法區(Metaspace)的垃圾。觸發頻率低,速度慢(因為老年代對象存活率高,可能采用標記-清除或標記-整理算法),STW時間長,對應用性能影響大。應盡量避免頻繁Full GC。
- 對象分配與晉升:
- 新對象優先在Eden區分配。
- 當Eden區滿,觸發Minor GC:
- 將Eden區和當前使用的Survivor區(如S0)中存活的對象,復制到另一個空閑的Survivor區(如S1)。
- 同時,給每個存活對象年齡+1(記錄在對象頭中)。
- 清空Eden區和剛使用過的Survivor區(S0)。
- S0和S1角色互換(原來的To變成新的From)。
- 當一個對象在Survivor區中“熬過”一定次數的Minor GC(年齡達到閾值
-XX:MaxTenuringThreshold
,默認15),它會被晉升 (Promotion) 到老年代。 - 如果Survivor區空間不足(無法容納Eden和另一個Survivor的存活對象),或者存活對象過大,會直接進入老年代(提前晉升或分配擔保失敗)。
- 當老年代空間不足時,會嘗試觸發Major GC/Full GC。
五、 垃圾收集器 - 算法實現者
JVM提供了多種垃圾收集器,適用于不同場景(吞吐量優先、低延遲優先、大堆內存等)。不同收集器可能用于不同分代。
-
Serial 收集器:
- 單線程工作。
- 進行GC時,必須暫停所有用戶線程(STW)。
- 簡單高效,沒有線程交互開銷。
- 應用場景: 客戶端模式或資源受限的嵌入式系統。
-XX:+UseSerialGC
(新生代Serial + 老年代Serial Old)。
-
ParNew 收集器:
- Serial收集器的多線程并行版本(僅作用于新生代)。
- 多線程并行進行標記和復制。
- GC時仍需STW。
- 應用場景: Server模式下與CMS收集器配合使用的主流新生代收集器。
-XX:+UseParNewGC
(需搭配老年代CMS)。
-
Parallel Scavenge / Parallel Old 收集器:
- 吞吐量優先收集器。
- Parallel Scavenge: 新生代收集器,多線程并行復制算法。
- Parallel Old: 老年代收集器,多線程并行標記-整理算法。
- 關注點:可控制的吞吐量 (用戶代碼運行時間 / (用戶代碼運行時間 + GC時間))。可通過
-XX:MaxGCPauseMillis
(最大GC停頓時間目標)和-XX:GCTimeRatio
(吞吐量目標)參數調節。 - 應用場景: 后臺運算、批處理任務等對吞吐量敏感的應用。
-XX:+UseParallelGC
/-XX:+UseParallelOldGC
。
-
CMS 收集器 (Concurrent Mark-Sweep):
- 低延遲優先收集器,目標是減少STW時間(尤其是老年代回收的停頓)。
- 老年代收集器,基于標記-清除算法。
- 過程 (四個主要階段):
- 初始標記 (Initial Mark): (STW) 標記GC Roots直接關聯的老年代對象。速度很快。
- 并發標記 (Concurrent Mark): GC線程與用戶線程并發執行,遍歷老年代對象圖進行可達性分析。
- 重新標記 (Remark): (STW) 修正并發標記期間因用戶線程繼續運行而導致標記產生變動的那一部分對象的標記記錄。比初始標記時間長,但遠短于并發標記。
- 并發清除 (Concurrent Sweep): GC線程與用戶線程并發執行,清除不可達對象。
- 優點: 并發階段(標記和清除)大大減少了STW時間。
- 缺點:
- CPU資源敏感: 并發階段占用線程,會與應用線程爭搶CPU。
- 浮動垃圾 (Floating Garbage): 并發清理階段用戶線程還在運行,會產生新的垃圾,只能留到下一次GC清理。
- 內存碎片: 標記-清除算法導致。可能觸發Full GC(Serial Old)進行碎片整理。
- Concurrent Mode Failure: 如果在并發清理完成前老年代空間被填滿(通常是晉升太快或浮動垃圾過多),會觸發Serial Old進行Full GC(導致長時間STW)。需預留足夠空間(
-XX:CMSInitiatingOccupancyFraction
)。
- 應用場景: 對響應時間敏感的B/S系統、Web服務等。
-XX:+UseConcMarkSweepGC
(新生代通常配合ParNew)。
-
G1 收集器 (Garbage-First):
- JDK 9+ 的默認收集器。目標是在可控的停頓時間內獲得盡可能高的吞吐量,并支持超大堆(數十GB甚至更大)。
- 核心思想: 將堆劃分為多個大小相等的獨立區域(Region)。G1跟蹤各個Region里垃圾堆積的“價值”(回收所得空間大小以及回收所需時間),在后臺維護一個優先列表。每次根據用戶設定的允許停頓時間(
-XX:MaxGCPauseMillis
,默認200ms),優先回收價值最大的Region(Garbage-First)。 - 特點:
- Region分區: 物理上不再連續分代,但邏輯上仍保留新生代、老年代概念(由一組Region組成)。有特殊的Humongous Region用于存放大對象。
- Mixed GC: G1除了常規的Young GC(只收集Eden/Survivor Region),還有一種Mixed GC模式。Mixed GC不僅收集新生代Region,也會根據預測模型選擇部分價值高的老年代Region進行收集。這是G1實現老年代回收的主要方式。
- 算法: 整體基于標記-整理算法,局部(Region之間)基于復制算法。避免了CMS的內存碎片問題。
- 可預測停頓模型: G1能建立停頓預測模型,有計劃地選擇部分Region進行回收,確保在指定的停頓時間內完成垃圾收集。
- Remembered Sets (RSet): 每個Region都有一個RSet,記錄其他Region中指向本Region內對象的引用。避免全堆掃描,使Region的回收相對獨立。
- Collection Sets (CSets): 每次GC時被選中回收的Region集合。
- 過程 (簡化):
- 初始標記 (Initial Mark): (STW) 標記GC Roots直接關聯的對象,并修改TAMS指針(為并發標記做準備)。通常與一次Young GC一起完成。
- 并發標記 (Concurrent Mark): 并發遍歷堆,進行可達性分析。
- 最終標記 (Final Marking): (STW) 處理SATB(Snapshot-At-The-Beginning)記錄,修正并發標記期間的變化。
- 篩選回收 (Evacuation): (STW) 根據停頓預測模型,選擇價值高的Region組成CSet,將CSet中存活的對象復制到空閑Region(復制算法),同時清空原Region(標記-整理效果)。這個階段是多線程并行進行的。
- 優點: 兼具高吞吐和低延遲潛力、無碎片、可管理超大堆。
- 應用場景: 需要低延遲、大內存的現代應用。
-XX:+UseG1GC
。
-
ZGC (Z Garbage Collector) 和 Shenandoah:
- 目標: 將STW時間控制在10ms以內,且與堆大小無關(亞毫秒級到10ms級),適用于超大堆(TB級)。
- 核心技術:
- 著色指針 (Colored Pointers): (ZGC) 在指針中嵌入元數據(標記位、重映射位等),避免傳統GC需要對象頭存儲標記信息的開銷。
- 讀屏障 (Read Barrier): 在應用程序線程讀取對象引用時,插入一小段代碼(屏障),配合著色指針實現并發轉移(對象移動時,應用線程通過屏障能感知到并訪問到對象的新地址)。
- 并發轉移 (Concurrent Relocation/Marking): GC線程與用戶線程并發地完成對象的標記和轉移(壓縮)。
- 特點: 幾乎全程并發,STW時間極短且固定(ZGC的STW主要發生在根掃描階段,與GC Roots數量有關,與堆大小無關)。
- 應用場景: 對延遲極其敏感的超大規模應用(金融交易、實時分析等)。
-XX:+UseZGC
(JDK 15+ Production Ready) /-XX:+UseShenandoahGC
(需要額外支持)。
六、 GC調優要點與常見問題
- 理解目標: 明確調優目標(吞吐量?低延遲?最小化內存占用?)。
- 選擇合適的收集器:
- 小應用/Client:Serial
- 吞吐量優先:Parallel Scavenge / Parallel Old
- 低延遲/響應優先:CMS (JDK 8及之前), G1 (JDK 9+ 默認且推薦)
- 極致低延遲/超大堆:ZGC / Shenandoah (JDK 11+)
- 調整堆大小:
-Xms
/-Xmx
:設置堆的初始大小和最大大小。通常設置成一樣大,避免堆自動擴展收縮帶來的開銷。- 過小:頻繁GC,影響吞吐量。
- 過大:單次GC停頓時間長,增加OS管理開銷。
- 調整新生代/老年代比例:
-XX:NewRatio
(老年代/新生代比例,默認2,即老年代是新生代的2倍)-XX:SurvivorRatio
(Eden/Survivor比例,默認8,即 Eden:Survivor=8:1)- 根據對象生命周期特點調整。短命對象多,增大新生代比例。
- 避免過早晉升:
- 增大
-XX:MaxTenuringThreshold
。 - 增大Survivor區大小(通過
-XX:SurvivorRatio
或直接設置-XX:SurvivorSize
)。 - 確保Survivor區能容納每次Minor GC后的存活對象。
- 增大
- 處理大對象:
- 避免創建過大的對象數組。
- 調整
-XX:PretenureSizeThreshold
,讓大對象直接進入老年代(減少在新生代的復制開銷)。
- 監控與分析:
- JVM參數:
-XX:+PrintGCDetails
,-XX:+PrintGCDateStamps
,-Xloggc:<file>
記錄GC日志。 - 工具:
jstat
:命令行查看JVM統計信息(GC次數、時間、各代容量使用率等)。- VisualVM:圖形化監控(堆、線程、GC等)。
- Java Mission Control (JMC):更強大的監控診斷工具(Flight Recorder)。
- GC日志分析工具: GCViewer, GCEasy, HPjmeter 等,可視化分析GC日志。
- JVM參數:
- 識別與解決內存泄漏:
- 現象: Full GC越來越頻繁,每次回收后老年代可用內存越來越少,最終OOM。
- 原因: 對象已不再使用,但由于意外的強引用(如靜態集合類長期持有、監聽器未注銷、未關閉的資源如Connection/Statement/Stream)導致無法被回收。
- 診斷工具:
jmap -histo:live <pid>
:查看堆直方圖(強制觸發Full GC)。jmap -dump:live,format=b,file=heapdump.hprof <pid>
:生成堆轉儲文件。- MAT (Memory Analyzer Tool), VisualVM Heap Dump Analyzer: 分析堆轉儲文件,查找支配樹、可疑引用鏈、大對象等。
七、 總結
Java的垃圾回收機制是其自動內存管理的核心,通過可達性分析判定對象生死,并主要采用分代收集思想結合多種算法(復制、標記-清除、標記-整理)來高效回收內存。不同的垃圾收集器(Serial, Parallel, CMS, G1, ZGC, Shenandoah)針對不同的性能目標(吞吐量、延遲、堆大小)進行了優化。理解GC的工作原理、內存模型、不同收集器的特點以及調優方法,對于開發高性能、高可靠的Java應用至關重要。持續監控GC行為,結合日志分析和堆轉儲工具定位問題,是優化應用內存使用和性能的有效手段。隨著硬件發展(大內存、多核)和應用需求(低延遲)的演進,GC技術(如ZGC, Shenandoah)也在不斷創新,追求更短的停頓時間和更大的堆管理能力。