當前位置:
首頁 > 知識 > JDK 源碼閱讀 Reference

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<T> {


    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<T> {


    // 會被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<T> {


    // 會被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類中:





// 用於控制垃圾回收器操作與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塊中啟動的:





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<T> {


    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<T> {


    /* 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技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

Map 大家族的那點事兒 ( 4 ) :HashMap
塵埃落定,JDK 11 確定將引入 Shebang #! 符號

TAG:ImportNew |