【JAVA Reference】ReferenceQueue 與 Reference 源碼剖析(二)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、架構

1.1 代碼架構圖

在這裏插入圖片描述

1.2 狀態流程圖

1.2.1 簡易流程圖

在這裏插入圖片描述
在這裏插入圖片描述

1.2.2 UML流程圖 (單ReferenceQueue)

在這裏插入圖片描述

1.2.3 UML流程圖 (多ReferenceQueue)

在這裏插入圖片描述

1.2.4 Reference對象的狀態通過成員變量next和queue來判斷
狀態 Next Queue discovered
Active null referenceQueue 下一個要回收的 reference(gc決定)(假如是最後一個,那麼就是 this )
Pending this referenceQueue 下一個要進入隊列的 reference (假如是最後一個,那麼就是 this )
Enqueued 隊列中下一個節點(假如是最後一個,next = this) ReferenceQueue.ENQUEUED null
Inactive this ReferenceQueue.NULL null

在 Active 與 Pending 狀態下的, discovered 就相當於 next。

1.2.5 未註冊 ReferenceQueue

如果未註冊 Queue,那麼就只有 Active 與 Inactive 狀態。
在這裏插入圖片描述

=== 點擊查看top目錄 ===

二、ReferenceQueue 源碼剖析

ReferenceQueue隊列是一個單向鏈表,ReferenceQueue裏面只有一個head 成員變量持有隊列的隊頭。後進先出的隊列,其實是個就是個棧!!!

2.1 ReferenceQueue 類

ReferenceQueue和 Reference 類都是 jdk1.2 的時候出的,所以也就不可能繼承jdk1.5出來的Queue接口

package java.lang.ref;
import java.util.function.Consumer;
public class ReferenceQueue<T> {

    public ReferenceQueue() { }

    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;  //內部類,它是用來做狀態識別的,重寫了enqueue入隊方法,永遠返回false,所以它不會存儲任何數據,見後面的NULL和ENQUEUED兩個標識成員變量
        }
    }

    /*
    	狀態State:Inactive
    	1. 當 Reference對象創建時沒有指定 queue,設置爲 NULL
    	2. 該 Reference 被拉出隊列的時候,設置爲 NULL
	*/ 
    static ReferenceQueue<Object> NULL = new Null<>();

    /*
    	狀態State:ENQUEUED
    	Reference已經被ReferenceHander線程從pending隊列移到queue裏面時。設置爲 ENQUEUED
    	所以ENQUEUED狀態不可能在調用 enqueue 方法
    */
    static ReferenceQueue<Object> ENQUEUED = new Null<>();

    static private class Lock { };
    // 鎖對象,只是用來這個類加鎖用
    private Lock lock = new Lock();

    // 頭節點(有存放數據)
    private volatile Reference<? extends T> head = null;
    // 隊列真實長度
    private long queueLength = 0;
	// 如果Reference創建時沒有指定隊列或Reference對象已經在隊列裏面了,則直接返回
    boolean enqueue(Reference<? extends T> r) { }

    private Reference<? extends T> reallyPoll() {}
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }
 	//將頭部第一個對象移出隊列並返回,如果隊列爲空,則等待timeout時間後,返回null,這個方法會阻塞線程
    public Reference<? extends T> remove(long timeout){}
    //將隊列頭部第一個對象從隊列中移除出來,如果隊列爲空則直接返回null(此方法不會被阻塞)
    public Reference<? extends T> remove(){return remove(0);}
    void forEach(Consumer<? super Reference<? extends T>> action) {}
}
2.1.1 Null 內部類
  • java.lang.ref.ReferenceQueue.Null
  • 內部類,它是用來做狀態識別的,重寫了enqueue入隊方法,永遠返回false,所以它不會存儲任何數據,見後面的NULL和ENQUEUED兩個標識成員變量
private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false; 
        }
    }

=== 點擊查看top目錄 ===

2.2 enqueue 方法

   boolean enqueue(Reference<? extends T> r) {
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            ReferenceQueue<?> queue = r.queue;
            // 如果Reference創建時沒有指定隊列或Reference對象已經在隊列裏面了,則直接返回
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;   //!!! 只有r的隊列是當前隊列才允許入隊
            r.queue = ENQUEUED; //將r的queue設置爲ENQUEUED狀態,標識Reference已經入隊
            r.next = (head == null) ? r : head;  // 往頭部插 ,類似於棧結構
            head = r; 
            queueLength++;
            if (r instanceof FinalReference) {
             	//如果r是一個FinalReference實例,那麼將FinalReference數量也+1
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }

=== 點擊查看top目錄 ===

2.3 poll 方法

    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }
  • reallyPoll 方法
    private Reference<? extends T> reallyPoll() {       /* Must hold lock */
        Reference<? extends T> r = head; // 彈出 head ,從頭部取,類似於棧結構,後進先出
        if (r != null) {
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            head = (rn == r) ? null : rn;
            r.queue = NULL; // 打個出隊列標誌 NULL 
            r.next = r; 
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

=== 點擊查看top目錄 ===

2.4 remove 方法

  • remove(long timeout)
 // 刪掉頭節點,隊列是空的話,等待N秒
    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;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            for (;;) {
                lock.wait(timeout);
                r = reallyPoll();
                if (r != null) return r;
                if (timeout != 0) {
                    long end = System.nanoTime();
                    timeout -= (end - start) / 1000_000;
                    if (timeout <= 0) return null;
                    start = end;
                }
            }
        }
    }
  • remove()
    // 刪掉頭節點,隊列是空的話,一直等待下去, 等待notify
    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    }

=== 點擊查看top目錄 ===

2.5 forEach() 方法

這是jdk1.8 新增的接口,傳入一個 Consumer,調用 foreach 消費隊列中全部的Reference。

    void forEach(Consumer<? super Reference<? extends T>> action) {
        for (Reference<? extends T> r = head; r != null;) {
            action.accept(r);
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            if (rn == r) {
                if (r.queue == ENQUEUED) {
                    // still enqueued -> we reached end of chain
                    r = null;
                } else {
                    // already dequeued: r.queue == NULL; ->
                    // restart from head when overtaken by queue poller(s)
                    r = head;
                }
            } else {
                // next in chain
                r = rn;
            }
        }
    }

=== 點擊查看top目錄 ===

三、Reference 源碼剖析

3.1 Reference 類

這是 referece對象的抽象base類,這個類定義了所有 reference 對象的通用方法,因爲 reference 對象跟 GC垃圾。

/**
 * Abstract base class for reference objects.  This class defines the
 * operations common to all reference objects.  Because reference objects are
 * implemented in close cooperation with the garbage collector, this class may
 * not be subclassed directly.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
// 這是 referece對象的抽象base類,這個類定義了所有 reference 對象的通用方法,因爲 reference 對象跟 GC垃圾收集器密切合作。
public abstract class Reference<T> {
    private T referent;         /* Treated specially by GC */ 

    @SuppressWarnings("rawtypes")
    volatile Reference next;

    static private class Lock { }
    //lock成員變量是pending隊列的全局鎖,
    //1. tryHandlePending 加鎖出隊列
    //2. jvm垃圾回收器線程入pending隊列,往pending裏面添加Reference對象
    private static Lock lock = new Lock();

    //pending隊列, pending成員變量與後面的discovered對象一起構成了一個pending單向鏈表,注意這個成員變量是一個靜態對象,所以是全局唯一的,pending爲鏈表的頭節點,discovered爲鏈表當前Reference節點指向下一個節點的引用
    // 這個隊列是由jvm的垃圾回收器構建的,當對象除了被reference引用之外沒有其它強引用了,jvm的垃圾回收器就會將指向需要回收的對象的Reference都放入到這個隊列裏面
    private static Reference<Object> pending = null;

    //與成員變量pending一起組成pending隊列,指向鏈表當前節點的下一個節點
    transient private Reference<T> discovered;  /* used by VM */

    //ReferenceQueue隊列,ReferenceQueue並不是一個鏈表數據結構,它只持有這個鏈表的表頭對象header,這個鏈表是由Refence對象裏面的next成員變量構建起來的,next也就是鏈表當前節點的下一個節點(只有next的引用,它是單向鏈表)
    volatile ReferenceQueue<? super T> queue;

    //以下是ReferenceHander線程初始化並啓動的操作
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler"); //線程名稱爲Reference Handler
        handler.setPriority(Thread.MAX_PRIORITY); //線程有最高優先級
        handler.setDaemon(true);//設置線程爲守護線程;
        handler.start();
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }
    Reference(T referent) {this(referent, null);}

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

    static boolean tryHandlePending(boolean waitForNotify) {
        //從pending中移除的Reference對象
        Reference<Object> r;
        Cleaner c;
        try {
             //此處需要加全局鎖,因爲除了當前線程,gc線程也會操作pending隊列。GC 往pending隊列塞Reference,該方法是取出來
            synchronized (lock) {
                 //如果pending隊列不爲空,則將第一個Reference對象取出
                if (pending != null) {
                    r = pending;  //緩存pending隊列頭節點
                    c = r instanceof Cleaner ? (Cleaner) r : null; 
                    pending = r.discovered; //將頭節點指向discovered,discovered爲pending隊列中當前節點的下一個節點,這樣就把第一個頭結點出隊了
                    r.discovered = null;  //將當前節點的discovered設置爲null;當前節點出隊,不需要組成鏈表了;
                } else {
                    //如果pending隊列爲空
                    if (waitForNotify) { //若要等待被喚醒,那麼 wait,
                        lock.wait();
                    }
                    return waitForNotify; //直接返回
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }
        if (c != null) {
            // 如果從pending隊列出隊的r是一個Cleaner對象,那麼直接執行其clean()方法執行清理操作;
            c.clean();

             //注意這裏直接 return true返回,這裏已經不往下執行了,所以Cleaner對象是不會進入到隊列裏面的,給它設置ReferenceQueue的作用是爲了讓它能進入Pending隊列後被ReferenceHander線程處理;
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
         //將對象放入到它自己的ReferenceQueue隊列裏
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }


    public T get() {return this.referent;}
    public void clear() {this.referent = null;}
    public boolean isEnqueued() {return (this.queue == ReferenceQueue.ENQUEUED);}
    public boolean enqueue() {return this.queue.enqueue(this);}

    private static class ReferenceHandler extends Thread {
        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }
        static {
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }
        //從源碼中可以看出,這個線程在Reference類的static構造塊中啓動,並且被設置爲最高優先級和daemon狀態。此線程要做的事情就是不斷的的檢查pending是否爲null,如果pending不爲null,則將pending進行enqueue,否則線程進行wait狀態。
        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }
}

從註釋可以知道,該 Reference 類跟 GC回收密切相關。
=== 點擊查看top目錄 ===

3.2 Reference 的四種狀態

instance 實例對象就是一個 reference 對象

3.2.1 狀態Active
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.

該狀態下的 Reference 會收到 GC 收集器的特別對待。有時候,當 GC 收集器檢測到 referent 的可達性變更爲其他合適狀態的時候。它會把 instance 的狀態改變爲 Pending 或者 Inactive,這取決於你創建 instance 的時候,
有沒有註冊 ReferenceQueue,如果有(前者),那麼會將這個 instance 加到 pending-Reference 鏈表裏面去。(狀態改成 Pending)後者的話,就是沒註冊 ReferenceQueue 的話,那麼狀態變成(Inactive)
新創建的對象實例狀態都是 Active
3.2.2 狀態Pending
Pending: An element of the pending-Reference list, waiting to be
enqueued by the Reference-handler thread. Unregistered instances
are never in this state.

一個 pending-Reference list中的元素,等待 Reference-handler線程 安排進隊列之前就是這個 Pending 狀態。沒有註冊的 instance 永遠不可能到達這一狀態。

3.2.3 狀態Enqueued
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.

當創建 Reference 的時候有註冊 queue 的元素。當一個 instance 從 ReferenceQueue 中 remove 的時候,
Reference 狀態變成 Inactive。沒有註冊ReferenceQueue的不可能到達這一狀態的。

3.2.4 狀態Inactive
Inactive: Nothing more to do. Once an instance becomes Inactive its
state will never change again.

沒啥事可做。一旦一個 instance 變成 Inactive 狀態,那麼不會再改變了。
3.2.5 狀態總結
  1. 新創建的Reference實例就是Active的
  2. 進入pending隊列就是就是pending狀態
  3. 通過Reference-Handler線程放入queue隊列中,那麼就是Enqueued狀態
  4. 當對象出隊之後,那麼就會變爲inactive狀態
3.2.6 流程總結
  1. New 新創建對象。
  2. 如若GC收集器覺得應該垃圾回收,那麼放進 pending 隊列裏面。
    pending隊列, pending成員變量與後面的discovered對象一起構成了一個pending單向鏈表,(這個變量是一個靜態對象,所以是全局唯一的pending爲鏈表的頭節點,discovered爲鏈表當前Reference節點指向下一個節點的引用),這個隊列是由jvm的垃圾回收器構建的,當對象除了被reference引用之外沒有其它強引用了,jvm的垃圾回收器就會將指向需要回收的對象的Reference都放入到這個隊列裏面(好好理解一下這句話,注意是指向要回收的對象的Reference,要回收的對象就是Reference的成員變量refernt持有的對象,是referent 持有的對象要被回收,而不是Reference對象本身)。
  3. 從 pending 隊列進入ReferenceQueue(初始化的時候假如沒有指定 ReferenceQueue,直接進入步驟4)
    這個pending 隊列會由ReferenceHander線程來處理(ReferenceHander線程是jvm的一個內部線程,它也是Reference的一個內部類,它的任務就是將pending隊列中要被回收的Reference對象移除出來,如果Reference對象在初始化的時候傳入了ReferenceQueue隊列,那麼就把從pending隊列裏面移除的Reference放到它自己的ReferenceQueue隊列裏。你可以操作這個隊列。除此之外ReferenceHander線程還會做一些其它操作
  4. 被回收,釋放內存空間。

=== 具體看狀態流程圖 ===

=== 點擊查看top目錄 ===

3.3 內部類ReferenceHandler

  • 內部類 java.lang.ref.Reference.ReferenceHandler
  • 該類繼承了 Thread類,爲毛不實現 Runnable ,我再想下?
  • 該類最主要的方法就是 Run 方法內部的 tryHandlePending 方法
private static class ReferenceHandler extends Thread {

    //確保類已經加載到 JVM 裏面去了
    private static void ensureClassInitialized(Class<?> clazz) {
        try {
            Class.forName(clazz.getName(), true, clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
        }
    }

    static {
        // 預先加載 InterruptedException 與 Cleaner 類,避免懶加載的時候,外面已經出現了內存問題。
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        while (true) { // 死循環
            tryHandlePending(true);
        }
    }
}

=== 點擊查看top目錄 ===

3.4 Reference#tryHandlePending() 方法

  • 該方法所做的,就是不斷輪訓遍歷 pending 隊列,然後丟入到 referenceQueue 隊列裏面去。
static boolean Reference#tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        // 加一個鎖,因爲除了這裏在操作出隊列, GC也在網pending隊列裏面塞數據
        synchronized (lock) {
            // 如果 pending 隊列不爲空
            if (pending != null) {
                r = pending;
                // 對 Cleaner 特殊處理
                c = r instanceof Cleaner ? (Cleaner) r : null;
                
                pending = r.discovered; // 指向pending的下一個 Reference
                r.discovered = null; // 置空
            } else {
                // 如果需要等待喚醒,那麼就進去睡覺
                if (waitForNotify) {
                    lock.wait();
                }
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) { // 這裏捕獲了OOM,表示還先別終止 JVM,再給他一次機會 
        // Give other threads CPU time so they hopefully drop some live references
        // and GC reclaims some space.
        // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
        // persistently throws OOME for some time...
        // 給其他線程一些cpu時間使得他們能夠丟掉一些活着的 reference 和讓cpu回收點空間。
        // 並且防止在繼承 Cleaner的時候 cpu 內部自旋
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) { // 就算被打斷又怎樣
        // retry
        return true;
    }

    // Fast path for cleaners
    // 如果是 cleaner 的話,那麼不進入隊列,直接調用 clean方法,然後回收
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;

    if (q != ReferenceQueue.NULL) q.enqueue(r); // queue有指定的話,入隊列
    return true;
}
  1. 看下里面的 OOM 錯誤處理,其實 try 內部的方法,並沒有方法會拋出異常,但是這裏捕獲 OOM,是想針對整個JVM的。先yield 讓出內存。
  2. 忽略線程的 InterruptedException。
  3. 爲什麼可以忽略 OOM 和 InterruptedException?爲什麼這麼屌? 因爲這個功能是給 GC 收集器使用的,肯定要保證一直活下去。
  • tryHandlePending 之後
    在這裏插入圖片描述

=== 點擊查看top目錄 ===

3.5 Reference的static代碼塊

//以下是ReferenceHander線程初始化並啓動的操作
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler"); //線程名稱爲Reference Handler
        handler.setPriority(Thread.MAX_PRIORITY); //線程有最高優先級
        handler.setDaemon(true);//設置線程爲守護線程;
        handler.start(); // 啓動內部類 Handler
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

Reference 類的 static 塊,handler.start(); 啓動了一個幽靈線程,這個線程就是 ReferenceHandler 線程類 ,它所做的,就是不斷輪訓遍歷 pending 隊列,然後丟入到 referenceQueue 隊列裏面去,即使遇到 OOM 或者 InterruptedException, 全部忽略。

=== 點擊查看top目錄 ===

3.6 Reference 構造方法

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

四、番外篇

下一章節:【JAVA Reference】Cleaner 源碼剖析(三)
上一章節:【JAVA Reference】Java的強引用、軟引用、弱引用和虛引用(一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章