垃圾收集算法-如何判定對象死亡

在堆中存放着幾乎所有的java對象實例,在GC執行垃圾回收之前,首先要區分出那些對象存活,哪些對象死亡,只有被標記爲死亡的對象,GC纔會在垃圾回收時釋放其所佔用的內存空間,這個過程被稱爲垃圾標記階段

在jvm中,當一個對象已經不再被任何存活的對象繼續引用時,就可以被宣判死亡,判斷對象存活一般有兩種方式:

  • 引用計數法
  • 可達性算法

1.不被java採用的引用計數法

引用計數法(Reference Counting),其實現過程相對簡單:對每個對象保存一個整形引用計算器屬性,用於記錄對象被引用的情況,舉個例子:對一個對象A,只要有一個對象引用了A,則A的引用計數器加1,當引用失效,引用計數器減1,當A計數器爲0,表示對象A不能再被引用,可進行回收
其優點就是實現簡單,垃圾對象易於辨識,判定效率高,回收沒有延遲,雖然它有這麼多優點,但爲什麼沒有被Java採用?,是因爲它有許多缺點:

  • 需要單獨的字段存儲計數器,增加了存儲的空間開銷
  • 伴隨着加法減法操作,帶來了時間開銷
  • 無法處理循環引用的問題,這是它的一個致命缺陷,這樣可能導致內存泄漏

那麼什麼是循環引用呢?看下面的圖:
**加粗樣式**
簡單來說:對象p指向了一個鏈表對象,而鏈表對象內部的計數器rc被循環引用,當p指向null時,由於循環引用導致rc不能清零導致無法被回收,進而導致內存泄漏,雖然java不採用這種方式,但某些其他語言採用了,比如Python,Python提供瞭解決循環引用導致內存泄漏的解決方案:

  • 手動解除
  • 使用弱引用

2.可達性算法

對於這種算法,有許多不同的稱呼,比如根搜索算法追蹤式垃圾收集(Tracing Garbage Collection),是java,c#等語言採用的判定對象死亡的垃圾收集算法,該算法可以有效解決循環引用問題,防止內存泄漏,其基本思路是:

  • 以根對象集合(GC Roots)爲起始點,按照從上至下的方式搜索被根對象集合所鏈接的對象是否可達
  • 使用可達性算法後,內存中的存活對象都會被根對象直接或間接連接着,搜索所走過的路徑稱爲引用鏈(Reference Chain)
  • 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象已經死亡,可以標記爲垃圾對象
  • 在可達性分析算法中,只有能被根對象集合直接或間接鏈接的對象纔是存活對象

在這裏插入圖片描述
在java中,GC Roots包括以下幾類元素:

  • 虛擬機棧中引用的對象(局部變量表中的引用指向堆中的對象),比如各個線程被調用的方法使用到的參數,局部變量等,這是最爲常見的一種
  • 方法區中類靜態屬性引用的對象,比如java類的引用類型靜態變量
  • 方法區中常量引用的對象,比如字符串常量池裏的引用
  • 本地方法棧引用的對象
  • 所有被同步鎖synchronized持有的對象
  • jvm虛擬機內部的引用:基本數據類型對應的Class對象,一些常駐的異常,系統類加載器
除了這些固定的GC Roots集合以外,根據用戶所選的垃圾收集器以及當前回收的內存區域不同,某些對象可臨時性的加入GC Roots集合,比如:分代收集和局部回收

3.對象的finalization機制

Java語言提供了對象終止(finalization)機制來允許開發人員提供對象銷燬前自定義處理邏輯,當垃圾回收器發現沒有一個引用指向對象,在回收這個對象之前先會調用這個對象的finalize()方法,這個方法是Object類中的方法:

  • protected void finalize() throws Throwable { }

該方法允許子類重寫,用於在對象被回收時進行資源釋放,比如關閉數據庫鏈接等,該方法在GC進行垃圾回收時由gc線程調用
注意:永遠不要主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用,因爲在執行這個方法的時候可能導致對象復活,並且該方法執行的時間時沒有保障的,它完全由GC線程決定,極端情況下若不發生GC,則該方法永遠不會被調用,一個糟糕的finalize()會嚴重影響GC性能

正是由於finalize()的存在,虛擬機中的對象一般處於三種狀態,可觸及的,可復活的,不可觸及的

  • 可觸及的,從根節點開始可以到達這個對象
  • 不可觸及的,對象的finalize()被調用,並且沒有復活,則進入不可觸及狀態,在這種狀態下對象不可能復活,因爲finalize()只能被調用一次
  • 可復活的,對象所有引用都被釋放,但對象在finalize()中可能復活

這三種狀態是由於finalize()的存在而進行的區分,只有對象在不可觸及狀態下才能被回收,通過GC垃圾回收器提供的Finalizer線程來處理,該線程優先級比較低

如果從所有根節點都無法訪問到某個對象,說明對象已經不再使用了,一般來說此對象需要被回收,但事實上,對象也並非必須立刻進行回收,一個無法觸及的對象可能在某一條件下‘復活’自己,在這種情況下再進行回收就顯得不合理了

我們通過一個例子來說明這個方法:

public class Alive {

    static Alive alive;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("調用重寫的finalize方法");
        alive = this;
    }

    public static void main(String[] args) {
        try {
            alive = new Alive();
            alive = null;//使對象沒有引用
            System.gc();//觸發回收
            System.out.println("第一次gc");
            Thread.sleep(2000);//等待Finalizer線程執行
            if(alive == null){
                System.out.println("對象已死");
            }else{
                System.out.println("對象存活");
            }
            System.out.println("第二次gc");
            alive = null;
            System.gc();//觸發回收
            Thread.sleep(2000);//等待Finalizer線程執行
            if(alive == null){
                System.out.println("對象已死");
            }else{
                System.out.println("對象存活");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

finalize()註釋前後分別進行測試:
註釋前:

第一次gc
對象已死
第二次gc
對象已死

註釋後:

調用重寫的finalize方法
第一次gc
對象存活
第二次gc
對象已死

對於以上結果我們可以做出如下分析:

在這裏插入圖片描述

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