從源碼理解Java虛引用

引用

在jdk1.2以後,java對引用的概念進行了擴充。將引用分爲了強引用,軟引用,弱引用,和虛引用四種。html

  • 強引用:即咱們日常說的引用,指在程序代碼中廣泛存在的引用賦值。在垃圾回收中,只要強引用還存在,那麼當前對象就永遠不會被回收。
  • 軟引用:比強引用弱一級的引用關係。在系統將要發生內存溢出前,會將軟引用關聯的對象歸入垃圾回收的範圍。
  • 弱引用:比軟引用更弱。被弱引用關聯的對象只能活到下一次垃圾回收前。
  • 虛引用:虛引用是最弱的一種引用關係,也被稱爲「幽靈引用」或「幻影引用」。一個對象是否有虛引用存在,對其生存不會產生任何影響,也沒法經過虛引用來取得一個對象實例

虛引用

上面已經說過:虛用於存在與否,不會對對象的生存產生任何影響,且也沒法經過虛引用來得到對象實例。java

那麼,虛引用到底有什麼用呢? bash

建立虛引用須要使用java.lang.ref.PhantomReference。app

咱們先看一下他的註釋:ide

/**
 * Phantom reference objects, which are enqueued after the collector
 * determines that their referents may otherwise be reclaimed.  Phantom
 * references are most often used for scheduling pre-mortem cleanup actions in
 * a more flexible way than is possible with the Java finalization mechanism.
 *
 * <p> If the garbage collector determines at a certain point in time that the
 * referent of a phantom reference is <a
 * href="package-summary.html#reachability">phantom reachable</a>, then at that
 * time or at some later time it will enqueue the reference.
 *
 * <p> In order to ensure that a reclaimable object remains so, the referent of
 * a phantom reference may not be retrieved: The <code>get</code> method of a
 * phantom reference always returns <code>null</code>.
 *
 * <p> Unlike soft and weak references, phantom references are not
 * automatically cleared by the garbage collector as they are enqueued.  An
 * object that is reachable via phantom references will remain so until all
 * such references are cleared or themselves become unreachable.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
複製代碼

被虛引用關聯的對象在收集器確認回收時會加入一個隊列。常常用來以一種靈活的方式定製對象在被回收前的清理工做。oop

若是gc已經肯定在特定時間點該虛引用對象是虛引用可達的,那麼就會將其加入隊列中。flex

爲了確保可回收對象保持不變,沒法經過虛引用獲取對象實例:虛引用的get方法 始終返回nullui

與軟引用和弱引用不一樣,虛引用不會在加入隊列時自動清除。直到全部此類引用已被清除或自己沒法訪問時,虛引用纔會被清除。this

PhantomReference的註釋中反覆提到了一個引用隊列,他就是java.lang.ref.ReferenceQueue。spa

ReferenceQueue

下面咱們重點分析一下ReferenceQueue

在瞭解一個類時,最簡單的辦法就是看他的註釋:

/**
 * Reference queues, to which registered reference objects are appended by the
 * garbage collector after the appropriate reachability changes are detected.
 *
 * @author   Mark Reinhold
 * @since    1.2
 */
複製代碼

引用隊列,在檢測到適當的可到達性變動後,垃圾回收器將已註冊的引用對象添加到該隊列中。

讓咱們看一下ReferenceQueue中入隊的代碼:

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        synchronized (lock) {
            // Check that since getting the lock this reference has not already been
            // enqueued (and even then removed)
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }
複製代碼

入隊代碼是比較簡單的,只是將入參Reference對象中的queue加入至當前隊列中,下面將用圖文描述調用enqueue後各個對象引用關係的變化:

在入隊前,對象的引用關係以下圖,在這裏,Reference對象中的queue屬性應該是指向ReferenceQueue對象的(assert queue == this):

在enqueue方法中,先將reference中的queue狀態設爲ENQUEUED,因爲目前隊列爲null,將haed和next屬性都執行入參reference對象,最後將queueLength加1。結果以下圖:

此時,當前ReferenceQueue長度爲1。

若是再有一個入隊請求的話,因爲隊列中已有一個元素,head不爲空,因此須要將next指針指向當前head指向的隊首元素,最後更新head指針爲當前入參reference,以下圖:

標爲黃色的爲本次新加入的reference對象,此時,queueLength==2。

經過上面的圖示,能夠看出,java中的referenceQueue其實是利用Reference中的next指針實現了一個先入先出的隊列。

而且,在入隊的過程當中,使用以下代碼來保證只有已註冊到當前隊列的對象才能夠進入當前referenceQueue實例:

ReferenceQueue<?> queue = r.queue;
    if ((queue == NULL) || (queue == ENQUEUED)) {
        return false;
    }
    assert queue == this;
複製代碼

那麼,如何將一個引用對象註冊到引用隊列中去呢?

其實,咱們已經經過代碼看出,判斷一個引用是否註冊到隊列中的依據就是當前引用是否持有對應隊列的引用。

Reference

進入抽象類java.lang.ref.Reference中,按照慣例,先看註釋:

/**
 * 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
 */
複製代碼

做爲引用對象的抽象基類,該類中定義了全部引用中的一些公用操做。由於引用對象是與gc緊密相關的,此類沒有直接父類。

根據註釋中來看,Reference存在四種內部狀態:

  • Active

    新建立的實例爲Active狀態。

  • Pending

    等待進入reference隊列。固然,未註冊到隊列中的實例永遠不會處於此狀態。

  • Enqueued

    已經成爲reference隊列中成員。一樣的,未註冊到隊列中的實例永遠不會處於此狀態。

  • Inactive

    終態,只要一個實例變爲此狀態就永遠不會再改變。

下面我畫了一個不太專業的狀態圖來表示四種狀態的變化關係:

引用由Pending狀態轉移到Enqueued狀態由後臺線程Reference-handler thread操做,那麼咱們來看一下這個線程都幹了些什麼:

在Reference中,jdk使用靜態代碼塊的方式去啓動Reference Handler線程:

static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }
複製代碼

能夠看出Reference Handler線程爲一個優先級爲MAX_PRIORITY的守護線程,其中ReferenceHandler是Reference中的一個私有靜態內部類。

ReferenceHandler類繼承了Thread類,並在run方法中實現了一個死循環:

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 {
            // pre-load and initialize InterruptedException and Cleaner classes
            // so that we don't get into trouble later in the run loop if there's
            // memory shortage while loading/initializing them lazily.
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

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

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }
複製代碼

爲了理解Reference Handler線程中的邏輯,咱們先看Reference中的幾個關鍵成員屬性:

/* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */
    transient private Reference<T> discovered;  /* used by VM */
複製代碼

discovered,私有transient變量,沒有任何地方給他賦值。註釋中也寫出他是給虛擬機用的,看上去虛擬機會在引用合適的狀態下給他賦對應的值。

/* Object used to synchronize with the garbage collector.  The collector
     * must acquire this lock at the beginning of each collection cycle.  It is
     * therefore critical that any code holding this lock complete as quickly
     * as possible, allocate no new objects, and avoid calling user code.
     */
    static private class Lock { }
    private static Lock lock = new Lock();
複製代碼

lock,鎖對象,供gc同步使用。在每次收集週期中,收集器必須獲取該鎖,所以,任何須要持有該鎖的代碼都應該儘快完成,儘量不分配任何對象和調用用戶代碼。

/* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;
複製代碼

pending,等待入隊的引用列表(Reference中維護有next指針,因此這裏說是list)。收集器會將引用加入至該list,同時,Reference-handler線程會將他們移出list。該list使用上面的lock對象做爲鎖,使用discovered變量去連接其元素。

下面我看Reference-handler中主要邏輯代碼: 先看tryHandlePending方法的註釋:

/**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
複製代碼

用來處理pending狀態的引用(若是有的話)。若是方法返回爲true,表示當前還有其餘狀態爲pending的引用,false則相反。程序能夠用此標識來肯定是否進入循環。

主要代碼以下(去除了異常處理邏輯):

Reference<Object> r;
    Cleaner c;
    synchronized (lock) {
            if (pending != null) {
                r = pending;
                // 'instanceof' might throw OutOfMemoryError sometimes
                // so do this before un-linking 'r' from the 'pending' chain...
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                pending = r.discovered;
                r.discovered = null;
            } else {
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    //...省略異常處理...
    //...省略異常處理...
    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }
    
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
    
複製代碼

根據pending變量和discovered的註釋,能夠畫出此時內存中的對象狀態:

pending爲等待入隊的變量列表,而discovered,當引用爲pending狀態時,爲pending List的下一個指針。 利用discovered對象,將pending指向pending_list鏈表中的下一個位置

(這裏注意下:這個鏈表是pending list,和上面說的referenceQueue不是一個東西):

若是r不是繼承至sun.misc.Cleaner的話,最後會將r加入引用隊列:

到這裏,咱們已經將本文開始處介紹的PhantomReference和ReferenceQueue都串連了起來:

對象從建立,註冊引用隊列,到最後被加入引用隊列的過程;

引用的四種內部狀態;

pending list,後臺線程Reference-handler的處理邏輯;

在最後,還看到了sun.misc.Cleaner類,在DirectByteBuffer中,就是利用Cleaner來完成堆外內存的清理。

參考資料

深刻理解java虛擬機

jdk8 相關源碼&註釋複製代碼
相關文章
相關標籤/搜索