在堆里面存放著 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549014.html
標籤:其他
上一篇:Rust如何引入原始碼作為依賴
