深入理解JVM(③)判斷對象是否還健在?

前言

因為Java對象主要存放在Java堆里,所以垃圾收集器(Garbage Collection)在對Java堆進行回收前,第一件事情就是要確定這些對象之中哪些還「存活」著,哪些已經「死去」(不被引用了)。

判斷對象是否健在的演算法

1.引用計數演算法

引用計數演算法,很容易理解,在對象中添加一個引用計數器,每有一個地方引用它時,計數器值就加一;當引用失效是,計數器值就減一;任何時刻計數器為零的對象就是不可以能再被使用的對象
引用計數演算法的原理簡單,判定效率也很高。市面上也確實有一些技術使用的此類演算法來判定對象是否存活,像ActionScript 3 的FlashPlayer、Python語言等。但是在主流的Java虛擬機裡面都沒有選用引用計演算法來管理記憶體,主要是使用此演算法時,必須要配合大量的額外處理才能保證正確的工作,例如要解決對象之間的相互循環引用的問題。

public class OneTest {

    public Object oneTest = null;

    private static final int _1MB = 1024 * 1024;

    private byte[] bigSize = new byte[256 * _1MB];


    /**
     * 這個成員屬性的唯一意義就是占點記憶體,以便能在GC日誌中看清楚是否有回收過。
     */
    @Test
    public void testGC(){

        OneTest test1 = new OneTest();
        OneTest test2 = new OneTest();

        test1.oneTest = test2;
        test2.oneTest = test1;

        test1 = null;
        test2 = null;

        // 假設在這行發生GC,test1和test2是否能被回收?
        System.gc();

    }

}

分析程式碼,test1和test2對象都被設置成了null,在後面發生GC的時候,如果按照引用計數演算法,這兩個對象雖然都被設置成了null,但是test1引用了test2,test2又引用了test1,所以這兩個對象的引用計數值都不為0,所以都不會被回收,但是真正的實際運行結果是,這兩個對象都被回收了,這也說明HotSpot虛擬機並不是用引用計數法來進行的記憶體管理。

2. 可達性分析演算法

當前主流的商用程式語言(Java、C#等),都是通過可達性分析(Reachability Analysis)演算法來判斷對象是否存活的。這個演算法的基本思路就是通過一一系列稱為「GC Roots」 的根對象作為起始節點集,從這些節點開始根據引用關係向下搜索,搜索走過的的路徑稱為「引用鏈」(Reference Chain),如果某個對象到GC Roots 間沒有任何引用鏈相連,或者從GC Roots 到這個對象不可達時,則證明此對象是不可能再被使用的。
如下圖,object10、object11、object12這三個對象,雖然互相有關聯,但是它們到GC Roots是不可達的,因此它們會被判定為可回收的對象。
可達性分析演算法
在Java程式中,固定可作為GC Roots 的對象包括以下幾種:

  • 在虛擬機棧(棧幀中的本地變數表)中引用的對象,譬如各個現場被調用的方法堆棧中使用到的參數、局部變數、臨時變數等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變數。
  • 在方法區中常量引用的對象,譬如字元串常量池(String Table)里的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(NullPointException、OutOfMemoryError)等,還有系統類載入器。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地程式碼快取等。
    除了這些固定的GC Roots集合以外,根據垃圾收集器以及當前回收的呢村區域不同,還會有其他對象「臨時性」的加入,如果只針對Java堆中某一塊兒區域發起垃圾收集時(例如只針對年輕代的垃圾收集),必須考慮到當前區域內的對象是否有被其他區域的對象所引用,這個時候就需要把這些關聯區域的對象一併加入GC Roots集合中,來保證可達性分析的正確性。

重申引用

無論是通過引用計數演算法判斷對象的引用數量,還是通過可達性分析演算法判斷對象是否引用鏈可達,判斷對象是否存活都和「引用」離不開關係。在JDK1.2之前,Java里對引用的概念是:如果reference類型的數據中存儲的數值代表的是另外一塊兒記憶體的地址,就稱該reference數據是代表某塊記憶體、某個對象的引用。
在JDK1.2版之後,Java對引用的概念進行了擴充,將引用分為強引用(Strongly Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  • 強引用是最傳統的「引用」的定義,指引用複製,即類似
Object obj = new Object()

這種引用關係。無論在任何情況下,只要強引用關係還存在,垃圾收集器就不會回收掉被引用的對象。

  • 軟引用是用來描述一些還有用,但非必須的對象。在系統發生記憶體溢出前,會先對軟引用對象進行第二次回收,如果回收後還沒有足夠的記憶體,才會拋出記憶體溢出的異常。
  • 弱引用也是用來描述那些非必須的對象,但是它的強度比軟引用更弱一些,弱引用的對象,只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用也稱為「幽靈引用」或「幻影引用」,它是最弱的一種引用關係。為一個對象設置虛引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。

判斷對象是生是死的過程

即使在可達性分析演算法中,判斷為不可達的對象,也不是「非死不可」的,要真正宣告一個對象死亡,至少要經歷兩次標記過程:

  • 如果第一次對象在進行可達性分析後發現與GC Roots 不可達,將進行第一次標記。
  • 隨後對此對象進行一次是否有必要執行finalize()方法進行篩選,假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,都視為「沒有必要執行」。
    如果對象被判定有必要執行finalize()方法,會將對象放置在一個名為F-Queue的隊列中,並在由一條由虛擬機自動建立的、低調度的執行緒區執行它們的finalize()方法。但並不承諾一定會等待它們運行結束。

需要注意的是:任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨第二次回收,它的finalize()方法不會被再次執行。
還有一點就是Java官方已經明確聲明不推薦手動調用finalize()方法了,因為它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,並且finanlize()能做的所有工作,使用try-finally或其他方式都可以做的更好、更及時。

回收方法區

方法區垃圾收集的「性價比」通常比較低,並且方法區回收也有過於苛刻的判定條件。
方法區的垃圾收集主要回收兩部分內容:廢棄的常量不再使用的類型,回收廢棄常量時,如果當前系統沒有一個常量的值是當前常量值,且虛擬機中也沒有其他地方引用這個常量。如果這個時候發生垃圾回收,常量就會被系統清理出常量池。
判定一個類型是否屬於「不再使用的類」的條件就比較苛刻了,要同時滿足如下三個條件:

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
  • 載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如OSGi、JSP的沖載入等,否則通常很難達成的。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

同時滿足了上述的三個條件後,也只是被允許進行回收了,關於是否要對類型進行回收還要對虛擬機進行一系列的參數設置,這裡就不贅述了,感興趣的可以自己去查詢。

Tags: