1. 程式人生 > >JVM----判斷物件是否存活 : 引用計數演算法OR可達性分析演算法?

JVM----判斷物件是否存活 : 引用計數演算法OR可達性分析演算法?

本篇來自周志明的<<深入理解java虛擬機器>>

在堆裡面存放著Java世界中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些物件之中哪些還“存活”著 ,哪些已經“死去”(即不可能再被任何途徑使用的物件)。

引用計數演算法

很多教科書判斷物件是否存活的演算法是這樣的:給物件中新增一個引用計數器,每當有—個地方引用它時,計數器值就加1 ; 當引用失效時,計數器值就減1 ; 任何時刻計數器為0的物件就是不可能再被使用的。

客觀地說,引用計數演算法( Reference Counting ) 的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的演算法,也有一些比較著名的應用案例,例如微軟公司的COM ( Component Object Model ) 技術、使用ActionScript 3的FlashPlayer、Python吾言和在遊戲指令碼領域被廣泛應用的Squirrel中都使用了引用計數演算法進行記憶體管理。但是 ,至少主流的Java虛擬機器裡面沒有選用引用計數演算法來管理記憶體,其中最主要的原因是它很難解決物件之間相互迴圈引用的問題。

舉個簡單的例子,請看程式碼清單3-1中的testGC() 方法:物件objA和objB都有欄位 instance , 賦值令objA.instance=objB及objB.instance=objA ,除此之外,這兩個物件再無任何引用 ,實際上這兩個物件已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為0 ,於是引用計數演算法無法通知GC收集器回收它們。

/**
 * 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();
    }
}

[Fu11 GC(System)[Tenured:0 K->210K(10240K),0.0149142 secs]4603K->21OK(19456K),[Perm:2999K-> 2999K(2124 8K )] ,0.0150007 secs] [Times :user=0.01 sys=0.00 ,real=0.02 secs ]

Heap def new generation total 9216K,used 82K[0x00000000055e0000 ,0x0000000005feO000 ,0x0000000005feOO00 ) Eden space 8192K ,llused[0x00000000055e0000 ,0x00000000055f4850 ,0x0000000005de0000 ) from space 1024K, Olusedf0x0000000005de0000 ,0x0000000005de0000 ,0x0000000005ee0000 ) to space 1024K ,0lused[0x0000000005ee0000 ,0x0000000005ee0000 ,0x0000000005fe0000 ) tenured generation total 1024OK,used 21OK[0x0000000005feO000 ,0x00000000069e0000 ,0x00000000069e0000 ) the space 10240K ,2lused[0x0000000005fe0000 ,0x0000000006014al8 ,0x0000000006014cO0 ,0x00000000069e0000 ) compacting perm gen total 21248K,used 3016K[0x00000000069e0000 ,0x0000000007ea0000 ,0x00000000ObdeO000 ) the space 21248K ,14lused[0x00000000069e0000 ,0x0000000006cd2398 ,0x0000000006cd2400 ,0x0000000007ea0000 ) Mo shared spaces configured.

從執行結果中可以清楚看到,GC日誌中包含“4603K->210K”,意味著虛擬機器並沒有因為這兩個物件互相引用就不回收它們,這也從側面說明虛擬機器並不是通過引用計數演算法來判斷物件是否存活的。

可達性分析演算法 

在主流的商用程式語言(Java、C # ,甚至包括前面提到的古老的Lisp ) 的主流實現中, 都是稱通過可達性分析( Reachability Analysis)來判定物件是否存活的。這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈( Reference Chain ) ,當一個物件到GC Roots沒有任何引用鏈相連 (用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。如圖3-1所示,物件object 5、 object 6、 object 7雖然互相有矣聯,但是它們到GC Roots是不可達的 ,所以它們將會被判定為是可回收的物件。

在Java語言中,可作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中類靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI ( 即一般說的Native方法)引用的物件。