一提到Reference 百分之九十九的java程式設計師都懵逼了

朱清震發表於2018-03-07

原來的標題是:"一提到Reference 99.99%的java程式設計師都懵逼了",為啥改成漢字了呢?吐槽一下,因為CSDN出bug了,如果你用了%做標題,你的文章就別想用它的編輯器修改了,它的js指令碼寫的不夠健壯,報錯了;


java.lang.ref.Reference

java程式設計師都知道如果一個物件沒有任何引用了,那麼這個物件在gc的時候就被回收了,大部分java程式設計師是基於我們常見的強引用去理解引用的,強引用這種方式雖然簡單直接,但是對於一些特殊的java物件,如快取資料在記憶體緊張時自動釋放掉空間防止oom、直接記憶體物件回收之前需自動釋放掉其佔用的堆外記憶體,當socket物件被回收之前關閉連線,當檔案流物件被回收之前自動關閉開啟的檔案等操作,強引用就無能為力了,為了實現這些特殊需求,java還引入了除了強引以外的引用型別來輔助物件回收操作 或 監控物件的生存週期。

先來看看引用型別的大家族:

在java.lang.ref包下除了ReferenceQueue類,Finalizer、FinalReference、PhantomReference、SoftReference、WeakReference都直接或間接繼承了Reference類,想想為什麼沒有強引用StrongReference?



理解Reference的關鍵點在於:

1. 兩個佇列pending與ReferenceQueue的作用;

2. Reference物件4種狀態的轉換;

3. ReferenceHander執行緒的處理過程,它是如何將Pending與ReferenceQueue關聯起來的;

4. Pending佇列資料來源;

理解了以上4個問題,才算真不再對Reference懵逼了,下面就帶著這四個問題去看下面的內容。

Reference構造方法

我們先來看看這些引用型別的父類都給我們提供了什麼?

其內部提供2個建構函式,除了傳入referent物件外,區別就是要不要傳入一個ReferenceQueue佇列

  Reference(Treferent) {

        this(referent,null);

   }

 

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

        this.referent =referent;

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

    }

ReferenceQueue佇列的作用就是Reference引用的物件被回收時,Reference物件能否進入到pending佇列,最終由ReferenceHander執行緒處理後,Reference就被放到了這個佇列裡面(Cleaner物件除外,後面有一篇專門講Cleaner物件原始碼),然後我們就可以在這個ReferenceQueue裡拿到reference,執行我們自己的操作(至於什麼操作就看你想怎麼用了),所以這個佇列起到一個物件被回收時通知的作用;

如果不帶ReferenceQueue的話,要想知道Reference持有的物件是否被回收,就只有不斷地輪訓reference物件,通過判斷裡面的get是否為null(phantomReference物件不能這樣做,其get始終返回null,因此它只有帶queue的建構函式).這兩種方法均有相應的使用場景,取決於實際的應用.如weakHashMap中就選擇去查詢queue的資料,來判定是否有物件將被回收.而ThreadLocalMap,則採用判斷get()是否為null來作處理;

對於帶ReferenceQueue引數的構造方法,如果傳入的佇列為null,那麼就會給成員變數queue賦值為ReferenceQueue.NULL佇列,這個NULL是ReferenceQueue物件的一個繼承了ReferenceQueue的內部類,它重寫了入隊方法enqueue,這個方法只有一個操作,直接返回 false,也就是這個對列不會存取任何資料,它起到狀態標識的作用;


Reference的重要成員變數

//在構造方法傳入java物件最後就賦值給了referent,也就是它引用到的物件

private Treferent;        /* Treated specially by GC */

  //pending佇列, pending成員變數與後面的discovered物件一起構成了一個pending單向連結串列,注意這個成員變數是一個靜態物件,所以是全域性唯一的,pending為連結串列的頭節點,discovered為連結串列當前Reference節點指向下一個節點的引用,這個佇列是由jvm的垃圾回收器構建的,當物件除了被reference引用之外沒有其它強引用了,jvm的垃圾回收器就會將指向需要回收的物件的Reference都放入到這個佇列裡面(好好理解一下這句話,注意是指向要回收的物件的Reference,要回收的物件就是Reference的成員變數refernt持有的物件,是refernt持有的物件要被回收,而不是Reference物件本身),這個佇列會由ReferenceHander執行緒來處理(ReferenceHander執行緒是jvm的一個內部執行緒,它也是Reference的一個內部類,它的任務就是將pending佇列中要被回收的Reference物件移除出來,如果Reference物件在初始化的時候傳入了ReferenceQueue佇列,那麼就把從pending佇列裡面移除的Reference放到它自己的ReferenceQueue佇列裡(為什麼是它自己的?pending佇列是全域性唯一的佇列,但是Reference的queue卻不是,它是在構造方法裡面指定的,前面說過這裡Cleaner物件是個特例),如果沒有ReferenceQueue佇列,那麼其關聯的物件就不會進入到Pending佇列中,會直接被回收掉,除此之外ReferenceHander執行緒還會做一些其它操作,後面會講到

 /* List of References waiting to beenqueued.  The collector adds

     * References to this list, while theReference-handler thread removes

     * them. This list is protected by the above lock object. The

     * list uses the discovered field to linkits elements.

     */

    privatestatic Reference<Object>pending =null;

  //與成員變數pending一起組成pending佇列,指向連結串列當前節點的下一個節點

   /* When active:   next element in a discovered reference listmaintained by GC (or this if last)

     *    pending:   next element in thepending list (or null if last)

     *  otherwise:   NULL

     */

transientprivate Reference<T>discovered/* used by VM */

 

//ReferenceQueue佇列,這就是我們前面提到的起到通知作用的ReferenceQueue,需要注意的是ReferenceQueue並不是一個連結串列資料結構,它只持有這個連結串列的表頭物件header,這個連結串列是由Refence物件裡面的next成員變數構建起來的,next也就是連結串列當前節點的下一個節點(只有next的引用,它是單向連結串列),所以Reference物件本身就是一個連結串列的節點,這個連結串列資料來源前面講pending佇列的時候已經提到了,它是由ReferenceHander執行緒從pending佇列中取的資料構建的(需要注意的是,這個物件並不是一個全域性的,它是在構造方法裡面傳入進來的,所以Reference物件需要進入那個佇列是我們自己指定的,也有特例,如FinalReference,Cleaner就是一個內部全域性唯一的,無法指定,後面有兩篇專門講他們倆的原始碼),一旦Reference物件放入了佇列裡面,那麼queue就會被設定為ReferenceQueue.ENQUEUED,來標識當前Reference已經進入到隊裡裡面了;

   volatileReferenceQueue<?super T>queue;

 

  //用來與queue成員變數一同組成ReferenceQueue佇列,見上面queue的說明;

   /* When active:   NULL

     *     pending:  this

     *   Enqueued:   next reference inqueue (or this if last)

     *   Inactive:   this

     */

   @SuppressWarnings("rawtypes")

   Reference next;

 

 

 

    //lock成員變數是pending佇列的全域性鎖,如果你搜尋這個lock變數會發現它只在ReferenceHander執行緒run方法裡面用到了,不要忘了jvm垃圾回收器執行緒也會操作pending佇列,往pending裡面新增Reference物件,所以需要加鎖;

   /* Object used to synchronize with thegarbage collector.  The collector

     * must acquire this lock at the beginningof each collection cycle.  It is

     * therefore critical that any code holdingthis lock complete as quickly

     * as possible, allocate no new objects,and avoid calling user code.

     */

   staticprivateclass Lock { };

   privatestatic Locklock =new Lock();


這裡一定要理解pending佇列,什麼樣的Reference物件會進入這個佇列。

進入這個佇列的Reference物件需要滿足兩個條件:

1.    Reference所引用的物件已經不存在其它強引用;

2.    Reference物件在建立的時候,指定了ReferenceQueue



Reference狀態及其轉換

如果去檢視Reference原始碼,會發現這個類的開頭有一段很長的註釋,說明了Reference物件的四種狀態:

/* A Reference instance is in one of four possible internal states:
     *
     *     Active: Subject to special treatment by the garbage collector.  Some
     *     time after the collector detects that the reachability of the
     *     referent has changed to the appropriate state, it changes the
     *     instance's state to either Pending or Inactive, depending upon
     *     whether or not the instance was registered with a queue when it was
     *     created.  In the former case it also adds the instance to the
     *     pending-Reference list.  Newly-created instances are Active.
     *
     *     Pending: An element of the pending-Reference list, waiting to be
     *     enqueued by the Reference-handler thread.  Unregistered instances
     *     are never in this state.
     *
     *     Enqueued: An element of the queue with which the instance was
     *     registered when it was created.  When an instance is removed from
     *     its ReferenceQueue, it is made Inactive.  Unregistered instances are
     *     never in this state.
     *
     *     Inactive: Nothing more to do.  Once an instance becomes Inactive its
     *     state will never change again.
     *
     * The state is encoded in the queue and next fields as follows:
     *
     *     Active: queue = ReferenceQueue with which instance is registered, or
     *     ReferenceQueue.NULL if it was not registered with a queue; next =
     *     null.
     *
     *     Pending: queue = ReferenceQueue with which instance is registered;
     *     next = this
     *
     *     Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
     *     in queue, or this if at end of list.
     *
     *     Inactive: queue = ReferenceQueue.NULL; next = this.
     *
     * With this scheme the collector need only examine the next field in order
     * to determine whether a Reference instance requires special treatment: If
     * the next field is null then the instance is active; if it is non-null,
     * then the collector should treat the instance normally.
     *
     * To ensure that a concurrent collector can discover active Reference
     * objects without interfering with application threads that may apply
     * the enqueue() method to those objects, collectors should link
     * discovered objects through the discovered field. The discovered
     * field is also used for linking Reference objects in the pending list.
     */

如果你沒有耐心看完這段註釋,請直接往後看:

1.       Active:活動狀態,物件存在強引用狀態,還沒有被回收;

2.       Pending:垃圾回收器將沒有強引用的Reference物件放入到pending佇列中,等待ReferenceHander執行緒處理(前提是這個Reference物件建立的時候傳入了ReferenceQueue,否則的話物件會直接進入Inactive狀態);

3.       Enqueued:ReferenceHander執行緒將pending佇列中的物件取出來放到ReferenceQueue佇列裡;

4.       Inactive:處於此狀態的Reference物件可以被回收,並且其內部封裝的物件也可以被回收掉了,有兩個路徑可以進入此狀態,

路徑一:在建立時沒有傳入ReferenceQueue的Reference物件,被Reference封裝的物件在沒有強引用時,指向它的Reference物件會直接進入此狀態;

路徑二、此Reference物件經過前面三個狀態後,已經由外部從ReferenceQueue中獲取到,並且已經處理掉了。

Reference物件的狀態只需要通過成員變數next和queue來判斷:

1.    Active:   next=null

2.    Pending:  next = this ,queue = ReferenceQueue

3.    Enqueued:  queue =ReferenceQueue.ENQUEUED

4.    Inactive:    next = this  ,queue = ReferenceQueue.NULL; 

以下是Reference物件狀態轉換圖,用word畫的,將就著看吧:

特例還是要拿出來單獨說,上面的圖不適用描述Cleaner物件,Cleaner物件是沒有Enqueue狀態的,它經過HandReference處理時執行其clean方法清理,然後就直接進入了inactive狀態了;

下面簡單看一下他們的原始碼實現,如果你理解了上面所有內容,下面的原始碼理解起來非常簡單,我只做簡單的註釋說明:

ReferenceHandler執行緒原始碼解析

ReferenceHandler執行緒是一個擁有最高優先順序的守護執行緒,它是Reference類的一個內部類,在Reference類載入執行cinit的時候被初始化並啟動;它的任務就是當pending佇列不為空的時候,迴圈將pending佇列裡面的頭部的Reference移除出來,如果這個物件是個Cleaner例項,那麼就直接執行它的clean方法來執行清理工作;否則放入到它自己的ReferenceQueue裡面;

所以這個執行緒是pending佇列與ReferenceQueue的橋樑;


/* High-priority thread to enqueue pending References*/

   private static class ReferenceHandler extends Thread {

 

       ReferenceHandler(ThreadGroup g, String name) {

           super(g, name);

       }

 

       public void run() {

           for (;;) {

                //從pending中移除的Reference物件

               Reference<Object> r;

               //此處需要加全域性鎖,因為除了當前執行緒,gc執行緒也會操作pending佇列

               synchronized (lock) {

                    //如果pending佇列不為空,則將第一個Reference物件取出

                    if (pending != null) {

                        //快取pending佇列頭節點

                        r = pending;

                        //將頭節點指向discovered,discovered為pending佇列中當前節點的下一個節點,這樣就把第一個頭結點出隊了

                        pending = r.discovered;

                        //將當前節點的discovered設定為null;當前節點出隊,不需要組成連結串列了;

                        r.discovered = null;

                    } else {//如果pending佇列為空,則等待

                        try {

                            try {

                                lock.wait();

                            } catch(OutOfMemoryError x) { }

                        } catch(InterruptedException x) { }

                        continue;

                    }

               }

               // 如果從pending佇列出隊的r是一個Cleaner物件,那麼直接執行其clean()方法執行清理操作;

               if (r instanceof Cleaner) {

                    ((Cleaner)r).clean();

                   //注意這裡,這裡已經不往下執行了,所以Cleaner物件是不會進入到佇列裡面的,給它設定ReferenceQueue的作用是為了讓它能進入Pending佇列後被ReferenceHander執行緒處理;

                    continue;

                }

               //將物件放入到它自己的ReferenceQueue佇列裡

               ReferenceQueue<Object> q = r.queue;

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

           }

       }

    }

   //以下是ReferenceHander執行緒初始化並啟動的操作

   static {

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

        for (ThreadGroup tgn = tg;

             tgn != null;

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

       //執行緒名稱為Reference Handler

        Thread handler = newReferenceHandler(tg, "Reference Handler");

        /* If there were a special system-onlypriority greater than

         * MAX_PRIORITY, it would be used here

         */

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

       handler.setPriority(Thread.MAX_PRIORITY);

        //設定執行緒為守護執行緒;

        handler.setDaemon(true);

        handler.start();

}

ReferenceQueue原始碼解析

ReferenceQueue佇列是一個單向連結串列,ReferenceQueue裡面只有一個header成員變數持有佇列的隊頭,Reference物件是從隊頭做出隊入隊操作,所以它是一個後進先出的佇列

public class ReferenceQueue<T> {

 

   public ReferenceQueue() { }

    //內部類,它是用來做狀態識別的,重寫了enqueue入隊方法,永遠返回false,所以它不會儲存任何資料,見後面的NULL和ENQUEUED兩個標識成員變數

    private static class Null<S> extendsReferenceQueue<S> {

       boolean enqueue(Reference<? extends S> r) {

           return false;

       }

    }

    //當Reference物件建立時沒有指定queue或Reference物件已經處於inactive狀態

staticReferenceQueue<Object> NULL = new Null<>();

//當Reference已經被ReferenceHander執行緒從pending佇列移到queue裡面時

   static ReferenceQueue<Object> ENQUEUED = new Null<>();

 

staticprivate class Lock { };

//出隊入隊時對佇列加鎖

privateLock lock = new Lock();

//佇列頭

privatevolatile Reference<? extends T> head = null;

//佇列長度

   private long queueLength = 0;

    //入隊操作,ReferenceHander呼叫此方法將Reference放入到佇列裡

   boolean enqueue(Reference<? extends T> r) { /* Called only byReference class */

          //加鎖操作佇列

synchronized(lock) {

           //如果Reference建立時沒有指定佇列或Reference物件已經在佇列裡面了,則直接返回

           ReferenceQueue<?> queue = r.queue;

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

               return false;

           }

                //只有r的佇列是當前佇列才允許入隊

           assert queue == this;

           //將r的queue設定為ENQUEUED狀態,標識Reference已經入隊

           r.queue = ENQUEUED;

           //從佇列頭部入隊

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

           head = r;

           //佇列裡面物件數量+1

           queueLength++;

           //如果r是一個FinalReference例項,那麼將FinalReference數量也+1

            if (r instanceof FinalReference) {

               sun.misc.VM.addFinalRefCount(1);

/**Vm.addFinalRefCount(int n)方法的原始碼

  publicstatic void addFinalRefCount(int n) {

       // The caller must hold lock to synchronize the update.

 

       finalRefCount += n;

       if (finalRefCount > peakFinalRefCount) {

           peakFinalRefCount = finalRefCount;

       }

}

可以通過sun.misc.VM.getFinalRefCount()和sun.misc.VM.getPeakFinalRefCount()來獲取FinalReference物件的當前數量和峰值數量

***/

           }

                //喚醒出隊操作的等待執行緒

           lock.notifyAll();

           return true;

       }

    }

    //Reference物件出隊操作,將頭部第一個物件移出佇列,並將佇列長度-1

   @SuppressWarnings("unchecked")

   private Reference<? extends T> reallyPoll() {       /* Must hold lock */

       Reference<? extends T> r = head;

       if (r != null) {

           head = (r.next == r) ?

               null :

               r.next; // Unchecked due to the next field having a raw type inReference

           r.queue = NULL;

           r.next = r;

           queueLength--;

           if (r instanceof FinalReference) {

               sun.misc.VM.addFinalRefCount(-1);

           }

           return r;

       }

       return null;

    }

 

//將佇列頭部第一個物件從佇列中移除出來,如果佇列為空則直接返回null(此方法不會被阻塞)

   public Reference<? extends T> poll() {

       if (head == null)

           return null;

       synchronized (lock) {

           return reallyPoll();

       }

    }

 

   //將頭部第一個物件移出佇列並返回,如果佇列為空,則等待timeout時間後,返回null,這個方法會阻塞執行緒

   public Reference<? extends T> remove(long timeout)

       throws IllegalArgumentException, InterruptedException

    {

       if (timeout < 0) {

           throw new IllegalArgumentException("Negative timeout value");

       }

       synchronized (lock) {

           Reference<? extends T> r = reallyPoll();

           if (r != null) return r;

           for (;;) {

               lock.wait(timeout);

               r = reallyPoll();

               if (r != null) return r;

                if (timeout != 0) return null;

           }

       }

    }

   public Reference<? extends T> remove() throws InterruptedException{

       return remove(0);

    }

}



相關文章