1. 前言
在 Java 開發過程中,我們常常聽到“垃圾回收”(Garbage Collection, GC)這一術語。JVM 通過垃圾回收機制自動管理內存,極大地簡化了程序員的內存控制負擔。然而,GC 究竟是如何判斷哪些對象該回收、哪些應保留的呢?這正是“對象存活判定”的關鍵所在。
對象存活判定方法的效率和準確性,直接關系到系統性能的高低。在內存緊張的場景中,一個不合理的回收策略可能導致頻繁 GC,甚至導致 OutOfMemoryError(OOM)錯誤。
Java 語言自誕生之初就非常重視內存安全問題,JVM 也隨著版本更新不斷優化垃圾回收算法。目前主流的 Java 8 就采用了更加高效的“可達性分析法”替代傳統的“引用計數法”,以規避引用循環等典型問題。
本系列文章將從基礎原理入手,深入剖析 JVM 如何判斷對象是否“還活著”,并配合圖解與示例代碼,幫助開發者更好地理解這一 GC 背后的核心機制。
閱讀后你將收獲:
-
JVM 內存模型與對象管理原理
-
引用計數法與可達性分析法的差異與優劣
-
如何用代碼分析對象是否會被 GC 回收
-
實用工具(MAT、JVisualVM)的分析技巧
讓我們從最基礎的問題開始——“對象存活判定”到底指的是什么?
2. 什么是對象存活判定?
對象存活判定(Object Liveness Detection)是指 JVM 在垃圾回收過程中判斷某個對象是否仍“有用”的一套邏輯機制。只有當 JVM 確認一個對象“無用”時,才會將其內存空間釋放。
那么,什么是“有用”或“無用”?這個標準并非由程序員顯式指定,而是 JVM 通過一定的算法推導得出。
從 JVM 的角度來看:
-
“有用”的對象:程序仍然可以訪問到該對象。
-
“無用”的對象:程序中不再有任何方式可以訪問到該對象。
這就涉及到兩個關鍵問題:
-
程序如何“訪問”一個對象?
-
JVM 如何判斷“是否還能訪問”?
為了解決這兩個問題,JVM 提供了兩種主要的判斷方式:
-
引用計數法(Reference Counting):為每個對象維護一個引用計數器。
-
可達性分析法(Reachability Analysis):從一組被稱為 GC Roots 的起點出發,遍歷對象圖。
這兩種方法各有利弊,也體現了 JVM 垃圾回收策略的演進方向。
在接下來的章節中,我們將逐一剖析這兩種算法的底層原理、適用場景與實現機制,并結合示意圖和代碼說明其工作方式。
3. 方法一:引用計數法
基本原理
引用計數法的核心思想非常直觀:
每當有一個地方引用該對象,其引用計數就加 1; 每當有一個引用失效,其引用計數就減 1; 當引用計數為 0 時,說明該對象“無人引用”,可以回收。
這一機制類似于手動管理內存語言(如 C++ 的智能指針),但在 Java 中并未采用這種方式作為主流實現。
示意圖
假設我們有以下引用關系:
A --> B --> C^ ||_____|
-
對象 A 引用了 B,B 引用了 C,C 又回頭引用了 B,構成循環引用。
-
即便 A 被回收,B 與 C 相互引用,導致引用計數不為 0,從而無法釋放。
這就是引用計數法的致命缺陷:無法處理對象之間的循環引用問題。
示例代碼
雖然 Java 官方并未公開支持引用計數 GC,但我們可以通過偽代碼演示其原理:
class MyObject {int refCount = 0;void addReference() {refCount++;}void removeReference() {refCount--;if (refCount == 0) {// 回收對象內存System.out.println("對象可以被回收");}}
}
使用時:
MyObject obj = new MyObject();
obj.addReference(); // 引用 +1
obj.removeReference(); // 引用 -1,若為0則可回收
當然,實際 JVM 中并未使用這種方法來管理對象生命周期。
優缺點分析
優勢 | 劣勢 |
---|---|
算法實現簡單,效率高 | 無法處理循環引用 |
回收實時性好 | 增加引用維護成本 |
易于實現跨語言互操作 | 與現代 JVM 架構不兼容 |
因此,雖然引用計數法在一些腳本語言(如 Python)或 C++ 的智能指針中應用較多,但在 Java JVM 中并未成為主流方法。
下一節我們將介紹 JVM 真正使用的對象存活判定方式:可達性分析法(Reachability Analysis)。
4. 方法二:可達性分析法
GC Roots 概念
可達性分析法(Reachability Analysis)是目前 Java 虛擬機中對象存活判定的主流算法。
該方法的核心思想是:通過從一組稱為 "GC Roots" 的起始節點出發,沿著對象引用鏈向下搜索,如果某個對象從 GC Roots 出發可達,則說明該對象是“活著”的;否則就會被判定為“死亡”。
GC Roots 的起始節點通常包括:
-
虛擬機棧(棧幀中的本地變量表)中引用的對象
-
方法區中類靜態屬性引用的對象
-
方法區中常量引用的對象
-
本地方法棧中 JNI 引用的對象
我們將在后續的章節中深入介紹這些 GC Roots 的類型。
分析流程
整個分析過程可以類比成遍歷一張“對象圖”:
-
建立對象引用圖(Object Graph) 所有對象通過引用連接形成有向圖,圖中的邊代表引用關系。
-
標記可達對象 從 GC Roots 出發,標記所有可到達的對象,形成“可達集合”。
-
未被標記的對象即為不可達對象 這些不可達的對象被視為垃圾,等待 GC 清理。
注意:即使對象不可達,JVM 并不會立刻回收它。 如果該對象覆蓋了
finalize()
方法,還會進入一次“F-Queue”隊列,被 GC 再次確認其是否真的不可用。
圖解說明
[GC Roots]|-------------------------| | |Obj1 Obj2 Obj3| |Obj4 Obj5Obj6 (無法從 GC Roots 到達)
-
Obj1~Obj5 均從 GC Roots 可達,為存活對象。
-
Obj6 無任何引用鏈連接至 GC Roots,被視為“死亡對象”。
示例代碼演示
雖然 JVM 自動完成對象圖的構建和遍歷,我們無法直接干預,但可以通過示例展示“對象是否可達”的效果:
public class ReachabilityDemo {static class Node {String name;Node reference;Node(String name) {this.name = name;}@Overrideprotected void finalize() throws Throwable {System.out.println(name + " 被回收了");}}public static void main(String[] args) {Node a = new Node("A");Node b = new Node("B");Node c = new Node("C");a.reference = b;b.reference = c;a = null; // 去除對 A 的強引用b = null; // 去除對 B 的強引用c = null; // 去除對 C 的強引用System.gc(); // 顯式請求 GCtry {Thread.sleep(1000); // 等待 GC 完成} catch (InterruptedException e) {e.printStackTrace();}}
}
輸出示例:
C 被回收了
B 被回收了
A 被回收了
說明 A、B、C 都在不可達狀態下被 GC 回收。
優勢與 JVM 的支持
優點 | 說明 |
---|---|
能解決循環引用問題 | 不依賴引用計數值,識別結構關系 |
更適合復雜對象圖 | 圖遍歷可適配大型堆場景 |
JVM 官方支持 | Java 8 及以后的所有主流 JVM 均基于該方法 |
下一節我們將具體介紹 GC Roots 中的各類節點來源,幫助大家更深入理解對象“可達”的起點到底是什么。
?
5. GC Roots 的類型
在上一節中我們提到,GC Roots 是可達性分析的起點。那么,GC Roots 到底是什么?哪些對象或引用屬于 GC Roots?理解 GC Roots 是掌握 JVM 垃圾回收機制的核心一步。
GC Roots 主要包括以下幾種類型的引用:
1. 虛擬機棧中的引用(局部變量表)
每個線程在執行方法時都會創建一個棧幀(Stack Frame),其中的局部變量表中保存著各種基本類型和對象引用。
public class StackReferenceDemo {public static void main(String[] args) {Object obj = new Object(); // obj 是 GC Root 引用System.gc();}
}
在這個例子中,obj
是定義在主方法中的局部變量,它保存在棧幀的局部變量表中,因此是 GC Roots。
2. 方法區中類靜態屬性引用的對象
靜態字段隨著類的加載而存在于方法區中,引用的對象也會被視為 GC Roots。
public class StaticReferenceDemo {private static Object staticObj = new Object(); // 屬于 GC Rootpublic static void main(String[] args) {System.gc();}
}
即使沒有局部變量引用 staticObj
,它依然不會被 GC,因為它是類的靜態屬性。
3. 方法區中常量引用的對象
常量池中的引用,如字符串常量等,也是 GC Roots 的一部分。
public class ConstantPoolDemo {public static void main(String[] args) {String str = "hello world"; // 字符串常量常駐內存System.gc();}
}
在這個例子中,字符串 "hello world" 常駐在運行時常量池中,是 GC Roots 的一部分,不會被回收。
4. 本地方法棧中的 JNI 引用(Native 引用)
如果 Java 程序調用了本地方法(如 C/C++ 實現的庫),這些 native 方法中持有的對象引用也會被當作 GC Roots。
public class JNIDemo {static {System.loadLibrary("native-lib");}public native void callNative();
}
雖然無法用 Java 展示 native 層引用的具體內容,但這些引用 JVM 會在 GC 時特殊處理。
5. 活躍線程
所有運行中的線程(如主線程、GC線程、后臺線程等)都是 GC Roots,因為它們自身的引用鏈天然“存活”。只有當線程執行結束、退出后,它們才會從 GC Roots 移除。
6. JVM 內部保留的系統類加載器
例如 sun.misc.Launcher$AppClassLoader
、ExtClassLoader
等,這些類加載器加載的類及其引用的對象會被視為 GC Roots。
7. JDK 特殊結構
如 System.in/out/err
、線程上下文類加載器、反射中的 Method/Field/Constructor
對象、線程組等。這些結構大多存在于系統級類中,使用時容易導致內存泄露。
總結 GC Roots 類型
GC Roots 類型 | 是否常見 | 是否手動可控 |
---|---|---|
虛擬機棧引用 | ? 常見 | ? 可控 |
靜態屬性引用 | ? 常見 | ? 可控 |
常量池引用 | ? 常見 | ? 不建議操作 |
JNI 本地引用 | ? 復雜 | ? 不建議操作 |
活躍線程引用 | ? 常見 | ? 不可控 |
類加載器引用 | ? 常見 | ? 不可控 |
系統類結構引用 | ? 隱蔽 | ? 不可控 |
理解 GC Roots 的種類不僅有助于判斷哪些對象能被 GC 回收,也對分析內存泄露、優化引用管理非常有幫助。
在下一節中,我們將進一步探索 Java 中的 finalize()
機制,以及對象“搶救”自己的最后機會。
6. Finalize 機制與固定對象
即使一個對象在 GC Roots 的可達性分析中被判定為“不可達”,也不代表它立刻會被回收。Java 提供了一個“臨終遺言”機制,即 finalize()
方法,使對象有一次自我拯救的機會。
6.1 什么是 finalize()
finalize()
是 java.lang.Object
類中的一個方法:
protected void finalize() throws Throwable {// 釋放資源或對象復活的鉤子方法
}
當對象第一次被判定為不可達時,GC 會檢查該對象是否覆蓋了 finalize()
方法,且該方法是否尚未被調用。如果滿足條件,JVM 會將該對象放入一個名為 Finalization Queue 的隊列中,由一個低優先級的 Finalizer 線程去執行其 finalize()
方法。
注意:每個對象的 finalize()
方法最多只會被調用一次。
6.2 finalize() 能做什么?
-
釋放資源:用于釋放文件句柄、關閉網絡連接等非內存資源(但不推薦這么用,推薦使用 try-with-resources)。
-
復活對象:對象在
finalize()
中如果再次賦值給 GC Roots 引用鏈中的某個變量,則對象會“復活”。
6.3 示例:對象的自我拯救
public class FinalizeRescueDemo {public static FinalizeRescueDemo OBJ = null;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize() 方法被調用");OBJ = this; // 對象復活!}public static void main(String[] args) throws InterruptedException {OBJ = new FinalizeRescueDemo();// 第一次 GC,對象有機會復活OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "對象存活" : "對象死亡");// 第二次 GC,finalize() 不會再被調用OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "對象存活" : "對象死亡");}
}
運行結果:
finalize() 方法被調用
對象存活
對象死亡
說明:第一次 GC 時 finalize()
被調用,OBJ
被重新引用,從而復活。第二次 GC 時不再執行 finalize()
,對象被真正回收。
6.4 finalize() 的問題與風險
-
不可控時機:執行時間不確定,依賴 GC。
-
影響性能:JVM 要維護一個隊列和額外線程。
-
風險隱患:對象復活邏輯可能導致資源泄露或更難以調試的 bug。
-
已被廢棄:Java 9 開始標注為
@Deprecated
,建議使用java.lang.ref.Cleaner
替代。
6.5 替代方案:Cleaner
import java.lang.ref.Cleaner;public class CleanerDemo {private static final Cleaner cleaner = Cleaner.create();static class Resource implements Runnable {@Overridepublic void run() {System.out.println("資源被清理");}}public static void main(String[] args) {Object obj = new Object();cleaner.register(obj, new Resource());}
}
Cleaner
提供了比 finalize()
更輕量、可控的資源清理方式,推薦在現代 Java 項目中使用。
7. 不同引用類型與垃圾回收行為
Java Reference類及其實現類深度解析:原理、源碼與性能優化實踐
Java 中定義了四種不同級別的引用類型:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference) 和 虛引用(Phantom Reference),它們在 JVM 中表現出不同的“生存權重”。理解這些引用類型對于資源緩存、內存優化和對象生命周期控制至關重要。
7.1 強引用(Strong Reference)
這是最常見的引用類型:
Object obj = new Object();
只要強引用還存在,GC 永遠不會回收該對象。
特性:
-
是默認引用類型。
-
會阻止 GC 回收所指向的對象。
示例:
public class StrongReferenceDemo {public static void main(String[] args) {Object obj = new Object();System.gc();System.out.println(obj != null ? "對象未被回收" : "對象被回收");}
}
輸出:對象未被回收
7.2 軟引用(Soft Reference)
軟引用是一種比較“溫柔”的引用。它在內存不足時才會被 GC 回收。
SoftReference<Object> softRef = new SoftReference<>(new Object());
常用于內存敏感的緩存。
示例:
import java.lang.ref.SoftReference;public class SoftReferenceDemo {public static void main(String[] args) {Object obj = new Object();SoftReference<Object> softRef = new SoftReference<>(obj);obj = null;System.gc();if (softRef.get() != null) {System.out.println("軟引用對象仍存活");} else {System.out.println("軟引用對象被回收");}}
}
注意:此示例中的回收依賴內存狀況,可能不會立即觸發。
7.3 弱引用(Weak Reference)
弱引用在 GC 時總是會被回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
特性:
-
非常適合使用在 ThreadLocal、元數據緩存等短生命周期場景。
示例:
import java.lang.ref.WeakReference;public class WeakReferenceDemo {public static void main(String[] args) {Object obj = new Object();WeakReference<Object> weakRef = new WeakReference<>(obj);obj = null;System.gc();if (weakRef.get() != null) {System.out.println("弱引用對象仍存活");} else {System.out.println("弱引用對象被回收");}}
}
輸出:弱引用對象被回收
7.4 虛引用(Phantom Reference)
虛引用無法通過 get()
方法訪問,被用于對象被回收時收到通知。
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
特點:
-
永遠不會阻止 GC。
-
常與
ReferenceQueue
配合使用。
示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo {public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);obj = null;System.gc();System.out.println("phantomRef.get(): " + phantomRef.get());System.out.println("是否進入 ReferenceQueue: " + (queue.poll() != null));}
}
輸出:
phantomRef.get(): null
是否進入 ReferenceQueue: true
說明:虛引用不會返回實際對象,只用于跟蹤對象是否已被 GC。
7.5 引用強度對比總結
引用類型 | 是否影響 GC 回收 | 典型用途 | 是否可通過 get() 訪問對象 |
---|---|---|---|
強引用 | 否 | 普通對象引用 | 是 |
軟引用 | 視內存情況而定 | 內存敏感緩存 | 是 |
弱引用 | 是 | ThreadLocal、臨時元數據 | 是 |
虛引用 | 是(立即回收) | 清理前回調通知 | 否 |
?
8. 垃圾收集器與對象存活判定策略
Java 虛擬機中的垃圾收集器(GC)負責自動管理堆內存,及時回收不再使用的對象。不同的垃圾收集器采用不同的算法和策略來判斷對象是否存活,從而決定是否回收。理解這些策略有助于優化程序性能和內存管理。
8.1 常見垃圾收集器簡介
收集器名稱 | 特點 | 適用場景 |
---|---|---|
Serial GC(串行收集器) | 單線程執行,簡單高效 | 適合小內存或單核環境 |
Parallel GC(并行收集器) | 多線程并行,吞吐量優先 | 多核服務器環境 |
CMS GC(并發標記清理) | 低停頓,標記與清理并發執行 | 對響應時間敏感的應用 |
G1 GC(Garbage First) | 分區管理,低停頓,適合大堆 | 大內存多核服務器 |
?
8.2 對象存活判定的核心機制
無論使用哪種收集器,對象的存活判定都基于“可達性分析”(Reachability Analysis):
-
從 GC Roots(如線程棧、靜態變量)開始,遍歷所有引用鏈。
-
能被引用鏈訪問到的對象被認為是存活的,不回收。
-
無法訪問的對象則被標記為可回收。
8.3 不同收集器的對象判定流程
Serial 和 Parallel 收集器
-
標記-清除(Mark-Sweep)或標記-復制(Mark-Copy)算法。
-
先暫停應用(Stop-The-World),從 GC Roots 開始標記存活對象。
-
清除未標記對象或復制存活對象到新空間。
CMS 收集器
-
采用多階段并發標記:
-
初始標記:暫停應用,標記直接可達對象。
-
并發標記:應用線程運行時,標記間接可達對象。
-
重新標記:短暫停止應用,完成標記遺漏部分。
-
并發清理:清理不可達對象。
-
G1 收集器
-
將堆劃分成多個固定大小的區域(Region)。
-
并發標記階段識別每個區域的存活對象數量。
-
優先回收存活對象少的 Region,減少停頓時間。
-
支持混合回收:回收年輕代和部分老年代。
8.4 代碼示例:指定收集器啟動參數
# 使用 Serial GC
java -XX:+UseSerialGC -Xmx512m -Xms512m MyApp# 使用 CMS GC
java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g MyApp# 使用 G1 GC
java -XX:+UseG1GC -Xmx4g -Xms4g MyApp
使用 VisualVM 或 JVisualVM 可以觀察不同收集器下堆內存對象的存活情況。
9. 總結與實踐建議
本文全面解析了 JVM 中對象存活判定的核心機制及其應用,包括可達性分析、引用類型、Finalize機制、垃圾收集器對判定策略的影響等關鍵內容。
9.1 對象存活判定的核心是“可達性分析”
-
通過從 GC Roots 出發遍歷引用鏈,判斷對象是否仍被程序訪問。
-
只有不可達對象才有回收資格,確保安全且高效的內存管理。
9.2 多種引用類型助力內存優化
-
強引用、軟引用、弱引用、虛引用各具特點,開發者可根據需求選擇不同引用,靈活控制對象生命周期和內存回收時機。
-
理解它們的差異,有助于避免內存泄漏和提升程序穩定性。
9.3 Finalize機制存在風險,應盡量避免
-
finalize()
方法雖可讓對象“復活”,但執行時機不確定,且影響性能。 -
推薦使用
java.lang.ref.Cleaner
替代,更加安全且高效。
9.4 不同垃圾收集器對對象存活判定實現有差異
-
串行、并行、CMS 和 G1 GC 等采用各自的標記算法和階段,平衡吞吐量與延遲。
-
了解垃圾收集器特性,合理配置 GC 參數,對提升系統性能至關重要。
9.5 實踐建議
-
在開發中,優先確保對象引用鏈清晰,避免意外的強引用導致內存泄漏。
-
結合軟弱引用,設計緩存等場景,提高內存利用率。
-
監控和調優垃圾收集器,配合性能分析工具,及時發現和解決內存相關問題。
-
避免依賴
finalize()
,轉用 Cleaner 和顯式資源管理。 -
對于大型應用,考慮采用 G1 或者更先進的收集器,兼顧響應和吞吐。
通過深入理解對象存活判定方法,開發者能更精準地控制內存管理,寫出高效、穩定的 Java 應用。
?