1. 程式人生 > >JVM(三)對象的生死判定和算法詳解

JVM(三)對象的生死判定和算法詳解

size 本地 被調用 數值 exceptio 收集器 循環引用 read 之前

好的文章是能把各個知識點,通過邏輯關系串連起來,讓人豁然開朗的同時又記憶深刻。

導讀:對象除了生死之外,還有其他狀態嗎?對象真正的死亡,難道只經歷一次簡單的判定?如何在垂死的邊緣“拯救”一個將死對象?判斷對象的生死存活都有那些算法?本文帶你一起找到這些答案。

在正式開始之前,我們先來了解一下垃圾回收。

GC介紹

GC:Garbage Collection,中文翻譯為垃圾回收。

GC的歷史

GC有著很長的歷史了,最初的GC算法發布於1960年(已經快有60年的歷史了),Lisp之父John McCarthy發布的,他是一名非常有名的黑客,也是人工智能之父,同時也是GC之父。

為什麽要學習GC?

1、排查內存溢出和內存泄露的問題。

2、系統調優,處理更高的並發瓶頸。

GC的作用

1、找到內存空間的垃圾。

2、回收垃圾。

對象生死判斷算法

垃圾回收的第一步就是判斷對象是否存活,只有“死去”的對象,才會被垃圾回收器所收回。

引用計數器算法

引用計算器判斷對象是否存活的算法是這樣的:給每一個對象設置一個引用計數器,每當有一個地方引用這個對象的時候,計數器就加1,與之相反,每當引用失效的時候就減1。

優點:實現簡單、性能高。

缺點:增減處理頻繁消耗cpu計算、計數器占用很多位浪費空間、最重要的缺點是無法解決循環引用的問題。

因為引用計數器算法很難解決循環引用的問題,所以主流的Java虛擬機都沒有使用引用計數器算法來管理內存。

來看一段循環引用的代碼:

public class ReferenceDemo {
    public Object instance = null;
    private static final int _1Mb = 1024 * 1024;
    private byte[] bigSize = new byte[10 * _1Mb]; // 申請內存
    public static void main(String[] args) {
        System.out.println(String.format("開始:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
        ReferenceDemo referenceDemo = new ReferenceDemo();
        ReferenceDemo referenceDemo2 = new ReferenceDemo();
        referenceDemo.instance = referenceDemo2;
        referenceDemo2.instance = referenceDemo;
        System.out.println(String.format("運行:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
        referenceDemo = null;
        referenceDemo2 = null;
        System.gc(); // 手動觸發垃圾回收
        System.out.println(String.format("結束:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
    }
}

運行的結果:

開始:117 M

運行中:96 M

結束:119 M

從結果可以看出,虛擬機並沒有因為相互引用就不回收它們,也側面說明了虛擬機並不是使用引用計數器實現的。

可達性分析算法

在主流的語言的主流實現中,比如Java、C#、甚至是古老的Lisp都是使用的可達性分析算法來判斷對象是否存活的。

這個算法的核心思路就是通過一些列的“GC Roots”對象作為起始點,從這些對象開始往下搜索,搜索所經過的路徑稱之為“引用鏈”。

當一個對象到GC Roots沒有任何引用鏈相連的時候,證明此對象是可以被回收的。如下圖所示:

技術分享圖片

在Java中,可作為GC Roots對象的列表:

  • Java虛擬機棧中的引用對象。
  • 本地方法棧中JNI(既一般說的Native方法)引用的對象。
  • 方法區中類靜態常量的引用對象。
  • 方法區中常量的引用對象。

對象生死與引用的關系

從上面的兩種算法來看,不管是引用計數法還是可達性分析算法都與對象的“引用”有關,這說明:對象的引用決定了對象的生死。那對象的引用都有那些呢?

在JDK1.2之前,引用的定義很傳統:如果reference類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表著一塊引用。

這樣的定義很純粹,但是也很狹隘,這種情況下一個對象要麽被引用,要麽沒引用,對於介於兩者之間的對象顯得無能為力。

JDK1.2之後對引用進行了擴充,將引用分為:

  • 強引用(Strong Reference)
  • 軟引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虛引用(Phantom Reference)

這也就是文章開頭第一個問題的答案,對象不是非生即死的,當空間還足夠時,還可以保留這些對象,如果空間不足時,再拋棄這些對象。很多緩存功能的實現也符合這樣的場景。

強引用、軟引用、弱引用、虛引用,這4種引用的強度是依次遞減的。

強引用:在代碼中普遍存在的,類似“Object obj = new Object()”這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用:是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當jvm認為內存不足時,才會去試圖回收軟引用指向的對象。jvm會確保在拋出OutOfMemoryError之前,清理軟引用指向的對象。

弱引用:非必需對象,但它的強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。

虛引用:也稱為幽靈引用或幻影引用,是最弱的一種引用關系,無法通過虛引用來獲取一個對象實例,為對象設置虛引用的目的只有一個,就是當著個對象被收集器回收時收到一條系統通知。

死亡標記與拯救

在可達性算法中不可達的對象,並不是“非死不可”的,要真正宣告一個對象死亡,至少要經歷兩次標記的過程。

如果對象在進行可達性分析之後,沒有與GC Roots相連接的引用鏈,它會被第一次標記,並進行篩選,篩選的條件是此對象是否有必要執行finalize()方法。

執行finalize()方法的兩個條件:

1、重寫了finalize()方法。

2、finalize()方法之前沒被調用過,因為對象的finalize()方法只能被執行一次。

如果滿足以上兩個條件,這個對象將會放置在F-Queue的隊列之中,並在稍後由一個虛擬機自建的、低優先級Finalizer線程來執行它。

對象的“自我拯救”

finalize()方法是對象脫離死亡命運最後的機會,如果對象在finalize()方法中重新與引用鏈上的任何一個對象建立關聯即可,比如把自己(this關鍵字)賦值給某個類變量或對象的成員變量。

來看具體的實現代碼:

public class FinalizeDemo {
    public static FinalizeDemo Hook = null;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("執行finalize方法");
        FinalizeDemo.Hook = this;
    }
    public static void main(String[] args) throws InterruptedException {
        Hook = new FinalizeDemo();
        // 第一次拯救
        Hook = null;
        System.gc();
        Thread.sleep(500); // 等待finalize執行
        if (Hook != null) {
            System.out.println("我還活著");
        } else {
            System.out.println("我已經死了");
        }
        // 第二次,代碼完全一樣
        Hook = null;
        System.gc();
        Thread.sleep(500); // 等待finalize執行
        if (Hook != null) {
            System.out.println("我還活著");
        } else {
            System.out.println("我已經死了");
        }
    }
}

執行的結果:

執行finalize方法

我還活著

我已經死了

從結果可以看出,任何對象的finalize()方法都只會被系統調用一次。

不建議使用finalize()方法來拯救對象,原因如下:

1、對象的finalize()只能執行一次。

2、它的運行代價高昂。

3、不確定性大。

4、無法保證各個對象的調用順序。

參考

《深入理解Java虛擬機》

《垃圾回收的算法與實現》

※ 為寫好一篇技術文章,背後是讀了兩本書的“艱辛”。寫作不易,請多支持!!!

最後

關註公眾號,發送“gc”關鍵字,領取《垃圾回收的算法與實現》學習資料。

技術分享圖片

技術分享圖片

JVM(三)對象的生死判定和算法詳解