1. 程式人生 > >JDK 原始碼閱讀 Reference

JDK 原始碼閱讀 Reference

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

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

引用與可達性分類

Java目前有4中引用型別:

強引用(Strong Reference):普通的的引用型別,new一個物件預設得到的引用就是強引用,只要物件存在強引用,就不會被GC。 軟引用(Soft Reference):相對較弱的引用,垃圾回收器會在記憶體不足時回收弱引用指向的物件。JVM會在丟擲OOME前清理所有弱引用指向的物件,如果清理完還是記憶體不足,才會丟擲OOME。所以軟引用一般用於實現記憶體敏感快取。 弱引用(Weak Reference):更弱的引用型別,垃圾回收器在GC時會回收此物件,也可以用於實現快取,比如JDK提供的WeakHashMap。 虛引用(Phantom Reference):一種特殊的引用型別,不能通過虛引用獲取到關聯物件,只是用於獲取物件被回收的通知。 相較於傳統的引用計數演算法,Java使用可達性分析來判斷一個物件是否存活。其基本思路是從GC Root開始向下搜尋,如果物件與GC Root之間存在引用鏈,則物件是可達的。物件的可達性與引用型別密切相關。Java有5中型別的可達性:

強可達(Strongly Reachable):如果執行緒能通過強引用訪問到物件,那麼這個物件就是強可達的。 軟可達(Soft Reachable):如果一個物件不是強可達的,但是可以通過軟引用訪問到,那麼這個物件就是軟可達的 弱可達(Weak Reachable):如果一個物件不是強可達或者軟可達的,但是可以通過弱引用訪問到,那麼這個物件就是弱可達的。 虛可達(Phantom Reachable):如果一個物件不是強可達,軟可達或者弱可達,並且這個物件已經finalize過了,並且有虛引用指向該物件,那麼這個物件就是虛可達的。 不可達(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:

1 2 3 4 5 6 7 8 9 10 public abstract class Reference { private T referent; // 會被GC特殊對待

// 獲取Reference管理的物件
public T get() {
    return this.referent;
}

// ...

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

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

實現特定的引用型別 使用者可以物件被回收後得到通知 第一個功能已經解釋過了,第二個功能是如何做到的呢?

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

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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中狀態:

Active:新建立的例項的狀態,由垃圾回收器進行處理,如果例項的可達性處於合適的狀態,垃圾回收器會切換例項的狀態為Pending或者Inactive。如果Reference註冊了ReferenceQueue,則會切換為Pending,並且Reference會加入pending-Reference連結串列中,如果沒有註冊ReferenceQueue,會切換為Inactive。 Pending:在pending-Reference連結串列中的Reference的狀態,這些Reference等待被加入ReferenceQueue中。 Enqueued:在ReferenceQueue佇列中的Reference的狀態,如果Reference從佇列中移除,會進入Inactive狀態 Inactive:Reference的最終狀態 Reference物件圖如下:

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

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

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

可以看一下程式碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public abstract class Reference { // 會被GC特殊對待 private T referent; // reference被回收後,當前Reference例項會被新增到這個佇列中 volatile ReferenceQueue<? super T> queue;

// 全域性唯一的pending-Reference列表
private static Reference<Object> pending = null;

// Reference為Active:由垃圾回收器管理的已發現的引用列表(這個不在本文討論訪問內)
// Reference為Pending:在pending列表中的下一個元素,如果沒有為null
// 其他狀態:NULL
transient private Reference<T> discovered;  /* used by VM */
// ...

}

ReferenceHandler執行緒

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // 用於控制垃圾回收器操作與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 (;;) {
        Reference<Object> r;
        // 獲取鎖,避免與垃圾回收器同時操作
        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,這裡就不會進行入隊操作了
        ReferenceQueue<Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
}

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

1 2 3 4 5 6 7 8 9 10 11 12 13 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也是一個連結串列:

1 2 3 4 public class ReferenceQueue { private volatile Reference<? extends T> head = null; // … } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 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的用途了:

1 2 3 4 5 6 7 8 9 10 public abstract class Reference { /* When active: NULL * pending: this * Enqueued: 指向ReferenceQueue中的下一個元素,如果沒有,指向this * Inactive: this */ Reference next;

// ...

} 總結

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

在這裡給大家提供一個學習交流的平臺,java架構師群: 867748702

具有1-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加群。

在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加群。

如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的可以加群。

加Java架構師進階交流群獲取Java工程化、高效能及分散式、高效能、深入淺出。高架構。 效能調優、Spring,MyBatis,Netty原始碼分析和大資料等多個知識點高階進階乾貨的直播免費學習許可權 都是大牛帶飛 讓你少走很多的彎路的 群號是: 867748702對了 小白勿進 最好是有開發經驗

注:加群要求

1、具有工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。

2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。

4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。

5.阿里Java高階大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!