JVM 垃圾回收GC Roots Tracing

1.跟搜索算法:

JVM中對內存進行回收時,需要判斷對象是否仍在使用中,可以通過GC Roots Tracing辨別。

定義:

通過一系列名爲”GCRoots”的對象作爲起始點,從這個節點向下搜索,搜索走過的路徑稱爲ReferenceChain,當一個對象到GCRoots沒有任何ReferenceChain相連時,(圖論:這個對象不可到達),則證明這個對象不可用。

 

可以作爲GC Root 引用點的是:

  1. JavaStack中的引用的對象。
  2. 方法區中靜態引用指向的對象。
  3. 方法區中常量引用指向的對象。
  4. Native方法中JNI引用的對象。

GC管理的主要區域是Java堆,一般情況下只針對堆進行垃圾回收。方法區、棧和本地方法區不被GC所管理,因而選擇這些區域內的對象作爲GC roots,被GC roots引用的對象不被GC回收。

一.GC如何判斷一個對象爲”垃圾”的

java堆內存中存放着幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”着,哪些已經“死去”。那麼GC具體通過什麼手段來判斷一個對象已經”死去”的?

1.引用計數算法(已被淘汰的算法)

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的。

目前主流的java虛擬機都摒棄掉了這種算法,最主要的原因是它很難解決對象 之間相互循環引用的問題。儘管該算法執行效率很高。

例如:在testGC()方法中,對象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外這兩個對象再無任何引用,實際上這兩個對象都已經不能再被訪問,但是它們因爲相互引用着對象方,異常它們的引用計數都不爲0,於是引用計數算法無法通知GC收集器回收它們。

2.可達性分析算法

目前主流的編程語言(java,C#等)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定爲是可回收的對象。

這裏寫圖片描述

二.被GC判斷爲”垃圾”的對象一定會回收嗎

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。(即意味着直接回收)

如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。

finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

代碼示例:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
 
    public void isAlive() {
        System.out.println("yes,i am still alive:)");
    }
 
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
 
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        // 對象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因爲finalize方法優先級很低,所以暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
        // 下面這段代碼與上面的完全相同,但是這次自救卻失敗了
        SAVE_HOOK = null;
        System.gc();
        // 因爲finalize方法優先級很低,所以暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no,i am dead:(");
        }
    }
}

運行結果: 

finalize mehtod executed!
yes,i am still alive:)
no,i am dead:( 

SAVE_HOOK對象的finalize()方法確實被GC收集器觸發過,並且在被收集前成功逃脫了。另外一個值得注意的地方是,代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因爲任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了。因爲finalize()方法已經被虛擬機調用過,虛擬機都視爲“沒有必要執行”。(即意味着直接回收)

finalize()方法

大致描述一下finalize流程:當對象變成(GC Roots)不可達時,GC會判斷該對象是否覆蓋了finalize方法,若未覆蓋,則直接將其回收。否則,若對象未執行過finalize方法,將其放入F-Queue隊列,由一低優先級線程執行該隊列中對象的finalize方法。執行finalize方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收,否則,對象“復活”。

垃圾回收器準備釋放內存的時候,會先調用finalize()。

       (1).對象不一定會被回收。

       (2).垃圾回收不是析構函數。

       (3).垃圾回收只與內存有關。

       (4).垃圾回收和finalize()都是靠不住的,只要JVM還沒有快到耗盡內存的地步,它是不會浪費時間進行垃圾回收

          之所以要使用finalize(),是存在着垃圾回收器不能處理的特殊情況。假定你的對象(並非使用new方法)獲得了一塊“特殊”的內存區域,由於垃圾回收器只知道那些顯示地經由new分配的內存空間,所以它不知道該如何釋放這塊“特殊”的內存區域,那麼這個時候Java允許在類中定義一個由finalize()方法。

      特殊的區域例如:1)由於在分配內存的時候可能採用了類似C語言的做法,而非JAVA的通常new做法。這種情況主要發生在native method中,比如native method調用了C/C++方法malloc()函數系列來分配存儲空間,但是除非調用free()函數,否則這些內存空間將不會得到釋放,那麼這個時候就可能造成內存泄漏。但是由於free()方法是在C/C++中的函數,所以finalize()中可以用本地方法來調用它。以釋放這些“特殊”的內存空間。2)又或者打開的文件資源,這些資源不屬於垃圾回收器的回收範圍。

      換言之,finalize()的主要用途是釋放一些其他做法開闢的內存空間,以及做一些清理工作。因爲在JAVA中並沒有提夠像“析構”函數或者類似概念的函數,要做一些類似清理工作的時候,必須自己動手創建一個執行清理工作的普通方法,也就是override Object這個類中的finalize()方法。例如,假設某一個對象在創建過程中會將自己繪製到屏幕上,如果不是明確地從屏幕上將其擦出,它可能永遠都不會被清理。如果在finalize()加入某一種擦除功能,當GC工作時,finalize()得到了調用,圖像就會被擦除。要是GC沒有發生,那麼這個圖像就會被一直保存下來。

        一旦垃圾回收器準備好釋放對象佔用的存儲空間首先會去調用finalize()方法進行一些必要的清理工作只有到下一次再進行垃圾回收動作的時候,纔會真正釋放這個對象所佔用的內存空間。

       在普通的清除工作中,爲清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這與C++"析構函數"的概念稍有牴觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都"應該"破壞。若將C++對象創建成一個本地對象,比如在堆棧中創建(在Java中是不可能的,Java都在堆中),那麼清除或破壞工作就會在"結束花括號"所代表的、創建這個對象的作用域的末尾進行。若對象是用new創建的(類似於Java),那麼當程序員調用C++的 delete命令時(Java沒有這個命令),就會調用相應的析構函數。若程序員忘記了,那麼永遠不會調用析構函數,我們最終得到的將是一個內存"漏洞",另外還包括對象的其他部分永遠不會得到清除。
  相反,Java不允許我們創建本地(局部)對象--無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,因爲垃圾回收器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由於存在垃圾回收機制,所以Java沒有析構函數。然而,隨着以後學習的深入,就會知道垃圾收集器的存在並不能完全消除對析構函數的需要,或者說不能消除對析構函數代表的那種機制的需要(原因見下一段。另外finalize()函數是在垃圾回收器準備釋放對象佔用的存儲空間的時候被調用的,絕對不能直接調用finalize(),所以應儘量避免用它)。若希望執行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價於C++的析構函數,只是沒後者方便。

      在C++中所有的對象運用delete()一定會被銷燬,而JAVA裏的對象並非總會被垃圾回收器回收。In another word, 1 對象可能不被垃圾回收,2 垃圾回收並不等於“析構”,3 垃圾回收只與內存有關。也就是說,並不是如果一個對象不再被使用,是不是要在finalize()中釋放這個對象中含有的其它對象呢?不是的。因爲無論對象是如何創建的,垃圾回收器都會負責釋放那些對象佔有的內存。

參考:

http://blog.csdn.net/u010744711/article/details/51371535

http://blog.csdn.net/time_hunter/article/details/12405127

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章