在堆裡面存放著 Java 世界中幾乎所有的對象實例,垃圾收集器在對 Java 堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”著,哪些已經“死去”(“死去”即不可能再被任何途徑使用的對象)。 有兩種判斷對象是否存活的演算法:引用計數演算法、可達性分析演算法。 ...
在堆裡面存放著 Java 世界中幾乎所有的對象實例,垃圾收集器在對 Java 堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”著,哪些已經“死去”(“死去”即不可能再被任何途徑使用的對象)。
有兩種判斷對象是否存活的演算法:引用計數演算法、可達性分析演算法。
- 引用計數演算法判斷對象是否存活的基本思路是:在對象中添加一個引用計數器,每當有一個地方引用該對象時,計數器的值就加一;當引用失效時,計數器的值就減一;任何時刻計數器為零的對象就是不可能再被使用的對象。
- 可達性分析演算法判斷對象是否存活的基本思路是:通過一系列被稱為 “GC Roots” 的根對象作為起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑被稱為 “引用鏈”(Reference Chain),如果某個對象到 GC Roots 間沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可能再被使用的對象。
引用計數演算法
引用計數演算法(Reference Counting)判斷對象是否存活的基本思路是:在對象中添加一個引用計數器,每當有一個地方引用該對象時,計數器的值就加一;當引用失效時,計數器的值就減一;任何時刻計數器為零的對象就是不可能再被使用的對象。
客觀地說,引用計數演算法雖然占用了一些額外的記憶體空間來進行計數,但引用計數演算法的原理簡單,判定效率也很高,在大多數情況下它都是一個不錯的演算法。也有一些比較著名的應用案例, 例如微軟 COM(Component Object Model)技術、使用 ActionScript3 的 FlashPlayer、Python 語言以及在游戲腳本領域得到許多應用的 Squirrel 中都使用了引用計數演算法進行記憶體管理。
但是,在 Java 領域,至少主流的 Java 虛擬機裡面都沒有選用引用計數演算法進行記憶體管理,主要原因是,這個看似簡單的演算法有很多例外情況要考慮,必須要配合大量的額外處理才能保證正確地工作,譬如單純的引用計數就很難解決對象之間相互迴圈引用的問題。
舉個簡單的例子,請看代碼清單 3-1 的 testGC() 方法:對象 objA 和 objB 都有欄位 instance,賦值令 objA.instance=objB 及 objB.instance=objA,除此之外,這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,但是因為它們互相引用著對方, 導致它們的引用計數都不為零,引用計數演算法也就無法回收它們。
代碼清單 3-1 引用計數演算法的缺陷
/**
* testGC()方法執行後, objA和objB會不會被GC呢?
*
* @author zzm
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個成員屬性的唯一意義就是占點記憶體, 以便能在GC日誌中看清楚是否有回收過
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設在這行發生GC, objA和objB是否能被回收?
System.gc();
}
}
可達性分析演算法
當前主流的商用程式語言(Java、C#,上溯至古老的 Lisp)的記憶體管理子系統,都是通過可達性分析(Reachability Analysis)演算法來判斷對象是否存活。
可達性分析演算法判斷對象是否存活的基本思路是:通過一系列被稱為 “GC Roots” 的根對象作為起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑被稱為 “引用鏈”(Reference Chain),如果某個對象到 GC Roots 間沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達)時,則證明此對象是不可能再被使用的對象。
如下圖所示,對象 object 5、object 6、object 7 雖然互有關聯,但是它們到 GC Roots 是不可達的,因此它們將會被判定為是可回收的對象。
在 Java 技術體系裡面,固定可作為 GC Roots 的對象包括以下幾種:
-
Java 虛擬機棧(棧幀中的本地變數表) 中引用的對象,譬如各個線程調用的方法堆棧中使用到的參數變數(方法定義時聲明的變數)引用的對象、局部變數(定義在方法中的變數)引用的對象、臨時對象(沒有變數引用的對象)等。
-
本地方法棧中 JNI(即通常所說的 Native 方法)引用的對象(非 Java 代碼中的對象)。
-
方法區中引用的對象:
- 方法區中類的靜態屬性(static 關鍵字)引用的對象,譬如 Java 類的引用類型靜態變數。
- 方法區中常量(static 和 final 關鍵字)引用的對象,譬如字元串常量池(String Table)里的引用。
-
所有被同步鎖(synchronized 關鍵字)持有的對象。
-
Java 虛擬機內部的引用,如基本數據類型對應的 Class 對象,一些常駐的異常對象(比如 NullPointExcepiton、 OutOfMemoryError)等,還有系統類載入器。
-
反映 Java 虛擬機內部情況的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等。
除了這些固定的 GC Roots 集合以外,根據用戶所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他對象 “臨時性” 地加入,共同構成完整的 GC Roots 集合。譬如後文將會提到的分代收集和局部回收(Partial GC),如果只針對 Java 堆中某一塊區域發起垃圾收集時(如最典型的只針對新生代的垃圾收集),必須考慮到記憶體區域是虛擬機自己的實現細節(在用戶視角里任何記憶體區域都是不可見的),更不是孤立封閉的,所以某個區域里的對象完全有可能被位於堆中其他區域的對象所引用,這個時候就需要將這些關聯區域的對象也一併加入 GC Roots 集合中去,這樣才能保證可達性分析的正確性。
參考資料
《深入理解 Java 虛擬機》第 3 章:垃圾收集器與記憶體分配策略 3.2 對象已死?
本文來自博客園,作者:真正的飛魚,轉載請註明原文鏈接:https://www.cnblogs.com/feiyu2/p/17283806.html