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

前言

由於Java對象主要存放在Java堆裏,因此垃圾收集器(Garbage Collection)在對Java堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」(不被引用了)。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 到這個對象不可達時,則證實此對象是不可能再被使用的。
以下圖,object十、object十一、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()

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

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

判斷對象是生是死的過程

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

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

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

回收方法區

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

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

同時知足了上述的三個條件後,也只是被容許進行回收了,關因而否要對類型進行回收還要對虛擬機進行一系列的參數設置,這裏就不贅述了,感興趣的能夠本身去查詢。隊列

相關文章
相關標籤/搜索