歡迎光臨
每天分享高質量文章

JDK 原始碼閱讀 Reference

(點選上方公眾號,可快速關註)


來源:木杉的部落格 ,

imushan.com/2018/08/19/java/language/JDK原始碼閱讀-Reference/

Java最初只有普通的強取用,只有物件存在取用,則物件就不會被回收,即使記憶體不足,也是如此,JVM會爆出OOME,也不會去回收存在取用的物件。

如果只提供強取用,我們就很難寫出“這個物件不是很重要,如果記憶體不足GC回收掉也是可以的”這種語意的程式碼。Java在1.2版本中完善了取用體系,提供了4中取用型別:強取用,軟取用,弱取用,虛取用。使用這些取用型別,我們不但可以控制垃圾回收器對物件的回收策略,同時還能在物件被回收後得到通知,進行相應的後續操作。

取用與可達性分類

Java目前有4中取用型別:

  1. 強取用(Strong Reference):普通的的取用型別,new一個物件預設得到的取用就是強取用,只要物件存在強取用,就不會被GC。

  2. 軟取用(Soft Reference):相對較弱的取用,垃圾回收器會在記憶體不足時回收弱取用指向的物件。JVM會在丟擲OOME前清理所有弱取用指向的物件,如果清理完還是記憶體不足,才會丟擲OOME。所以軟取用一般用於實現記憶體敏感快取。

  3. 弱取用(Weak Reference):更弱的取用型別,垃圾回收器在GC時會回收此物件,也可以用於實現快取,比如JDK提供的WeakHashMap。

  4. 虛取用(Phantom Reference):一種特殊的取用型別,不能透過虛取用獲取到關聯物件,只是用於獲取物件被回收的通知。

相較於傳統的取用計數演演算法,Java使用可達性分析來判斷一個物件是否存活。其基本思路是從GC Root開始向下搜尋,如果物件與GC Root之間存在取用鏈,則物件是可達的。物件的可達性與取用型別密切相關。Java有5中型別的可達性:

  1. 強可達(Strongly Reachable):如果執行緒能透過強取用訪問到物件,那麼這個物件就是強可達的。

  2. 軟可達(Soft Reachable):如果一個物件不是強可達的,但是可以透過軟取用訪問到,那麼這個物件就是軟可達的

  3. 弱可達(Weak Reachable):如果一個物件不是強可達或者軟可達的,但是可以透過弱取用訪問到,那麼這個物件就是弱可達的。

  4. 虛可達(Phantom Reachable):如果一個物件不是強可達,軟可達或者弱可達,並且這個物件已經finalize過了,並且有虛取用指向該物件,那麼這個物件就是虛可達的。

  5. 不可達(Unreachable):如果物件不能透過上述的幾種方式訪問到,則物件是不可達的,可以被回收。

物件的取用型別與可達性聽著有點亂,好像是一回事,我們這裡實體分析一下:

上面這個例子中,A~D,每個物件只存在一個取用,分別是:A-強取用,B-軟取用,C-弱取用,D-虛取用,所以他們的可達性為:A-強可達,B-軟可達,C-弱可達,D-虛可達。因為E沒有存在和GC Root的取用鏈,所以它是不可達。

在看一個複雜的例子:

  • A依然只有一個強取用,所以A是強可達

  • B存在兩個取用,強取用和軟取用,但是B可以透過強取用訪問到,所以B是強可達

  • C只能透過弱取用訪問到,所以是弱可達

  • D存在弱取用和虛取用,所以是弱可達

  • E雖然存在F的強取用,但是GC Root無法訪問到它,所以它依然是不可達。

同時可以看出,物件的可達性是會發生變化的,隨著執行時取用物件的取用型別的變化,可達性也會發生變化,可以參考下圖:

Reference總體結構

Reference類是所有取用型別的基類,Java提供了具體取用型別的具體實現:

  • SoftReference:軟取用,堆記憶體不足時,垃圾回收器會回收對應取用

  • WeakReference:弱取用,每次垃圾回收都會回收其取用

  • PhantomReference:虛取用,對取用無影響,只用於獲取物件被回收的通知

  • FinalReference:Java用於實現finalization的一個內部類

因為預設的取用就是強取用,所以沒有強取用的Reference實現類。

Reference的核心

Java的多種取用型別實現,不是透過擴充套件語法實現的,而是利用類實現的,Reference類表示一個取用,其核心程式碼就是一個成員變數reference:

public abstract class Reference {

    private T referent; // 會被GC特殊對待

 

    // 獲取Reference管理的物件

    public T get() {

        return this.referent;

    }

 

    // …

}

如果JVM沒有對這個變數做特殊處理,它依然只是一個普通的強取用,之所以會出現不同的取用型別,是因為JVM垃圾回收器硬編碼識別SoftReference,WeakReference,PhantomReference等這些具體的類,對其reference變數進行特殊物件,才有了不同的取用型別的效果。

上文提到了Reference及其子類有兩大功能:

  1. 實現特定的取用型別

  2. 使用者可以物件被回收後得到通知

第一個功能已經解釋過了,第二個功能是如何做到的呢?

一種思路是在新建一個Reference實體是,新增一個回呼,當java.lang.ref.Reference#referent被回收時,JVM呼叫該回呼,這種思路比較符合一般的通知模型,但是對於取用與垃圾回收這種底層場景來說,會導致實現複雜,效能不高的問題,比如需要考慮在什麼執行緒中執行這個回呼,回呼執行阻塞怎麼辦等等。

所以Reference使用了一種更加原始的方式來做通知,就是把取用物件被回收的Reference新增到一個佇列中,使用者後續自己去從佇列中獲取並使用。

理解了設計後對應到程式碼上就好理解了,Reference有一個queue成員變數,用於儲存取用物件被回收的Reference實體:

public abstract class Reference {

    // 會被GC特殊對待

    private T referent; 

    // reference被回收後,當前Reference實體會被新增到這個佇列中

    volatile ReferenceQueue super T> queue;

 

    // 只傳入reference的建構式,意味著使用者只需要特殊的取用型別,不關心物件何時被GC

    Reference(T referent) {

        this(referent, null);

    }

 

    // 傳入referent和ReferenceQueue的建構式,reference被回收後,會新增到queue中

    Reference(T referent, ReferenceQueue super T> queue) {

        this.referent = referent;

        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;

    }

 

    // …

}

Reference的狀態

Reference物件是有狀態的。一共有4中狀態:

  1. Active:新建立的實體的狀態,由垃圾回收器進行處理,如果實體的可達性處於合適的狀態,垃圾回收器會切換實體的狀態為Pending或者Inactive。如果Reference註冊了ReferenceQueue,則會切換為Pending,並且Reference會加入pending-Reference連結串列中,如果沒有註冊ReferenceQueue,會切換為Inactive。

  2. Pending:在pending-Reference連結串列中的Reference的狀態,這些Reference等待被加入ReferenceQueue中。

  3. Enqueued:在ReferenceQueue佇列中的Reference的狀態,如果Reference從佇列中移除,會進入Inactive狀態

  4. Inactive:Reference的最終狀態

Reference物件圖如下:

除了上文提到的ReferenceQueue,這裡出現了一個新的資料結構:pending-Reference。這個連結串列是用來乾什麼的呢?

上文提到了,reference取用的物件被回收後,該Reference實體會被新增到ReferenceQueue中,但是這個不是垃圾回收器來做的,這個操作還是有一定邏輯的,如果垃圾回收器還需要執行這個操作,會降低其效率。從另外一方面想,Reference實體會被新增到ReferenceQueue中的實效性要求不高,所以也沒必要在回收時立馬加入ReferenceQueue。

所以垃圾回收器做的是一個更輕量級的操作:把Reference新增到pending-Reference連結串列中。Reference物件中有一個pending成員變數,是靜態變數,它就是這個pending-Reference連結串列的頭結點。要組成連結串列,還需要一個指標,指向下一個節點,這個對應的是java.lang.ref.Reference#discovered這個成員變數。

可以看一下程式碼:

public abstract class Reference {

    // 會被GC特殊對待

    private T referent; 

    // reference被回收後,當前Reference實體會被新增到這個佇列中

    volatile ReferenceQueue super T> queue; 

 

    // 全域性唯一的pending-Reference串列

    private static Referencepending = null;

 

    // Reference為Active:由垃圾回收器管理的已發現的取用串列(這個不在本文討論訪問內)

    // Reference為Pending:在pending串列中的下一個元素,如果沒有為null

    // 其他狀態:NULL

    transient private Reference discovered;  /* used by VM */

    // …

}

ReferenceHandler執行緒

透過上文的討論,我們知道一個Reference實體化後狀態為Active,其取用的物件被回收後,垃圾回收器將其加入到pending-Reference連結串列,等待加入ReferenceQueue。這個過程是如何實現的呢?

這個過程不能對垃圾回收器產生影響,所以不能在垃圾回收執行緒中執行,也就需要一個獨立的執行緒來負責。這個執行緒就是ReferenceHandler,它定義在Reference類中:

// 用於控制垃圾回收器操作與Pending狀態的Reference入隊操作不衝突執行的全域性鎖

// 垃圾回收器開始一輪垃圾回收前要獲取此鎖

// 所以所有佔用這個鎖的程式碼必須儘快完成,不能生成新物件,也不能呼叫使用者程式碼

static private class Lock { };

private static Lock lock = new Lock();

 

private static class ReferenceHandler extends Thread {

 

    ReferenceHandler(ThreadGroup g, String name) {

        super(g, name);

    }

 

    public void run() {

        // 這個執行緒一直執行

        for (;;) {

            Referencer;

            // 獲取鎖,避免與垃圾回收器同時操作

            synchronized (lock) {

                // 判斷pending-Reference連結串列是否有資料

                if (pending != null) {

                    // 如果有Pending Reference,從串列中取出

                    r = pending;

                    pending = r.discovered;

                    r.discovered = null;

                } else {

                    // 如果沒有Pending Reference,呼叫wait等待

                    // 

                    // wait等待鎖,是可能丟擲OOME的,

                    // 因為可能發生InterruptedException異常,然後就需要實體化這個異常物件,

                    // 如果此時記憶體不足,就可能丟擲OOME,所以這裡需要捕獲OutOfMemoryError,

                    // 避免因為OOME而導致ReferenceHandler行程靜默退出

                    try {

                        try {

                            lock.wait();

                        } catch (OutOfMemoryError x) { }

                    } catch (InterruptedException x) { }

                    continue;

                }

            }

 

            // 如果Reference是Cleaner,呼叫其clean方法

            // 這與Cleaner機制有關係,不在此文的討論訪問

            if (r instanceof Cleaner) {

                ((Cleaner)r).clean();

                continue;

            }

 

            // 把Reference新增到關聯的ReferenceQueue中

            // 如果Reference構造時沒有關聯ReferenceQueue,會關聯ReferenceQueue.NULL,這裡就不會進行入隊操作了

            ReferenceQueueq = r.queue;

            if (q != ReferenceQueue.NULL) q.enqueue(r);

        }

    }

}

ReferenceHandler執行緒是在Reference的static塊中啟動的:

static {

    // 獲取system ThreadGroup

    ThreadGroup tg = Thread.currentThread().getThreadGroup();

    for (ThreadGroup tgn = tg;

         tgn != null;

         tg = tgn, tgn = tg.getParent());

    Thread handler = new ReferenceHandler(tg, “Reference Handler”);

 

    // ReferenceHandler執行緒有最高優先順序

    handler.setPriority(Thread.MAX_PRIORITY);

    handler.setDaemon(true);

    handler.start();

}

綜上,ReferenceHandler是一個最高優先順序的執行緒,其邏輯是從Pending-Reference連結串列中取出Reference,新增到其關聯的Reference-Queue中。

ReferenceQueue

Reference-Queue也是一個連結串列:

public class ReferenceQueue {

    private volatile Reference extends T> head = null;

    // …

}

// ReferenceQueue中的這個鎖用於保護連結串列佇列在多執行緒環境下的正確性

static private class Lock { };

private Lock lock = new Lock();

 

boolean enqueue(Reference extends T> r) { /* Called only by Reference class */

    synchronized (lock) {

        // 判斷Reference是否需要入隊

        ReferenceQueue > queue = r.queue;

        if ((queue == NULL) || (queue == ENQUEUED)) {

            return false;

        }

        assert queue == this;

 

        // Reference入隊後,其queue變數設定為ENQUEUED

        r.queue = ENQUEUED;

        // Reference的next變數指向ReferenceQueue中下一個元素

        r.next = (head == null) ? r : head;

        head = r;

        queueLength++;

        if (r instanceof FinalReference) {

            sun.misc.VM.addFinalRefCount(1);

        }

        lock.notifyAll();

        return true;

    }

}

 

透過上面的程式碼,可以知道java.lang.ref.Reference#next的用途了:

public abstract class Reference {

    /* When active:   NULL

     *     pending:   this

     *    Enqueued:   指向ReferenceQueue中的下一個元素,如果沒有,指向this

     *    Inactive:   this

     */

    Reference next;

 

    // …

}

總結

一個使用Reference+ReferenceQueue的完整流程如下:

參考資料

  • Java Reference詳解 – robin-yao的個人頁面 – 開源中國

    https://my.oschina.net/robinyao/blog/829983

  • Internals of Java Reference Object

    http://www.javarticles.com/2016/10/internals-of-java-reference-object.html

  • java.lang.ref (Java Platform SE 7 )

    https://docs.oracle.com/javase/7/docs/api/java/lang/ref/package-summary.html#reachability

  • Java Reference Objects

    http://www.kdgregory.com/index.php?page=java.refobj

  • Java核心技術36講 第4講

【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂