1. 程式人生 > >深入理解JDK中的Reference原理和原始碼實現

深入理解JDK中的Reference原理和原始碼實現

前提

這篇文章主要基於JDK11的原始碼和最近翻看的《深入理解Java虛擬機器-2nd》一書的部分內容,對JDK11中的Reference(引用)做一些總結。值得注意的是,通過筆者對比一下JDK11和JDK8對於java.lang.ref包的相關實現,發現程式碼變化比較大,因此本文的原始碼分析可能並不適合於JDK11之外的JDK版本。

Reference的簡介和分類

在JDK1.2之前,Java中的引用的定義是十分傳統的:如果reference型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。在這種定義之下,一個物件只有被引用和沒有被引用兩種狀態。

實際上,我們更希望存在這樣的一類物件:當記憶體空間還足夠的時候,這些物件能夠保留在記憶體空間中;如果當記憶體空間在進行了垃圾收集之後還是非常緊張,則可以拋棄這些物件。基於這種特性,可以滿足很多系統的快取功能的使用場景。

java.lang.ref包是JDK1.2引入的,包結構和類分佈如下:

- java.lang.ref
  - Cleaner.class
  - Finalizer.class
  - FinalizerHistogram.class
  - FinalReference.class
  - PhantomReference.class
  - Reference.class
  - ReferenceQueue.class
  - SoftReference.classs
  - WeakReference.class

引入此包的作用是對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)四種類型的引用,還有一種比較特殊的引用是析構引用(Final Reference),它是一種特化的虛引用。四種引用的強度按照下面的次序依次減弱:

StrongReference > SoftReference > WeakReference > PhantomReference

值得注意的是:

  • 強引用沒有對應的型別表示,也就是說強引用是普遍存在的,如Object object = new Object();
  • 軟引用、弱引用和虛引用都是java.lang.ref.Reference的直接子類。
  • 直到JDK11為止,只存在四種引用,這些引用是由JVM建立,因此直接繼承java.lang.ref.Reference建立自定義的引用型別是無效的,但是可以直接繼承已經存在的引用型別,如java.lang.ref.Cleaner
    就是繼承自java.lang.ref.PhantomReference
  • 特殊的java.lang.ref.Reference的子類java.lang.ref.FinalReferenceObject#finalize()有關,java.lang.ref.Finalizerjava.lang.ref.FinalReference子類,下文會詳細分析這些內容。

Reference

Reference就是引用,對JVM的垃圾收集活動敏感(當然,強引用可能對垃圾收集活動是不敏感的),Reference的繼承關係或者實現是由JDK定製,引用例項是由JVM建立,所以自行繼承Reference實現自定義的引用型別是無意義的,但是可以繼承已經存在的引用型別,如SoftReference等。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.

翻譯一下大意是:Reference是所有引用物件的基類。這個類定義了所有引用物件的通用操作。因為引用物件是與垃圾收集器緊密協作而實現的,所以這個類可能不能直接子類化。

Reference的狀態集合

Reference原始碼中並不存在一個成員變數用於描述Reference的狀態,它是通過組合判斷referent、discovered、queue、next成員的存在性或者順序"拼湊出"對應的狀態,註釋中描述如下:

一個引用物件可以同時存在兩種狀態:
- 第一組狀態:"active", "pending", or "inactive"
- 第二組狀態:"registered", "enqueued", "dequeued", or "unregistered"

Active:

當前引用例項處於Active狀態,會收到垃圾收集器的特殊處理。在垃圾收集器檢測到referent的可達性已更改為適當狀態之後的某個時間,垃圾收集器會"通知"當前引用例項改變其狀態為"pending"或者"inactive"。此時的判斷條件是:referent != null; discovered = null或者例項位於GC的discovered列表中。

Pending:

當前的引用例項是pending-Reference列表的一個元素,等待被ReferenceHandler執行緒處理。pending-Reference列表通過應用例項的discovered欄位進行關聯。此時的判斷條件是:referent = null; discovered = pending-Reference列表中的下一個元素

Inactive:

當前的引用例項處於非Active和非Pending狀態。此時的判斷條件是:referent = null (同時discovered = null)

Registered:

當前的引用例項建立的時候關聯到一個引用佇列例項,但是引用例項暫未加入到佇列中。此時的判斷條件是:queue = 傳入的ReferenceQueue例項

Enqueued:

當前的引用例項已經新增到和它關聯的引用佇列中但是尚未移除(remove),也就是呼叫了ReferenceQueue.enqueued()後的Reference例項就會處於這個狀態。此時的判斷條件是:queue = ReferenceQueue.ENQUEUE; next = 引用列表中的下一個引用例項,或者如果當前引用例項是引用列表中的最後一個元素,則它會進入Inactive狀態

Dequeued:

當前的引用例項曾經新增到和它關聯的引用佇列中並且已經移除(remove)。此時的判斷條件是:queue = ReferenceQueue.NULL; next = 當前的引用例項

Unregistered:

當前的引用例項不存在關聯的引用佇列,也就是建立引用例項的時候傳入的queue為null。此時的判斷條件是:queue = ReferenceQueue.NULL

狀態躍遷的時序圖如下:

     * Initial states:
     *   [active/registered]
     *   [active/unregistered] [1]
     *
     * Transitions:
     *                            clear
     *   [active/registered]     ------->   [inactive/registered]
     *          |                                 |
     *          |                                 | enqueue [2]
     *          | GC              enqueue [2]     |
     *          |                -----------------|
     *          |                                 |
     *          v                                 |
     *   [pending/registered]    ---              v
     *          |                   | ReferenceHandler
     *          | enqueue [2]       |--->   [inactive/enqueued]
     *          v                   |             |
     *   [pending/enqueued]      ---              |
     *          |                                 | poll/remove
     *          | poll/remove                     |
     *          |                                 |
     *          v            ReferenceHandler     v
     *   [pending/dequeued]      ------>    [inactive/dequeued]
     *
     *
     *                           clear/enqueue/GC [3]
     *   [active/unregistered]   ------
     *          |                      |
     *          | GC                   |
     *          |                      |--> [inactive/unregistered]
     *          v                      |
     *   [pending/unregistered]  ------
     *                           ReferenceHandler
     *
     * Terminal states:
     *   [inactive/dequeued]
     *   [inactive/unregistered]
     *
     * Unreachable states (because enqueue also clears):
     *   [active/enqeued]
     *   [active/dequeued]
     *
     * [1] Unregistered is not permitted for FinalReferences.
     *
     * [2] These transitions are not possible for FinalReferences, making
     * [pending/enqueued] and [pending/dequeued] unreachable, and
     * [inactive/registered] terminal.
     *
     * [3] The garbage collector may directly transition a Reference
     * from [active/unregistered] to [inactive/unregistered],
     * bypassing the pending-Reference list.

註釋中還強調了幾點:

  • 初始化狀態:[active/registered][active/unregistered](這種情況只限於FinalReferences)
  • 終結狀態:[inactive/dequeued][inactive/unregistered]
  • 不可能出現的狀態:[active/enqeued][active/dequeued]

上面的圖看起來可能比較抽象,ReferenceHandler其實是Reference中靜態程式碼塊中初始化的執行緒例項,主要作用是:處理pending狀態的引用例項,使它們入佇列並走向[inactive/dequeued]狀態。另外,上面的線框圖是分兩部分,其中上半部分是使用了ReferenceQueue,後半部分是沒有使用ReferenceQueue(或者說使用了ReferenceQueue.NULL)。這裡嘗試用PPT畫一下簡化的狀態躍遷圖:

Reference原始碼分析

先看Reference的建構函式和成員變數:

public abstract class Reference<T> {
   private T referent;
   volatile ReferenceQueue<? super T> queue;
   volatile Reference next;
   private transient Reference<T> discovered;

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

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

構造描述:

建構函式依賴於一個泛型的referent成員以及一個ReferenceQueue<? super T>的佇列,如果ReferenceQueue例項為null,則使用ReferenceQueue.NULL

成員變數描述:

  • referent:Reference儲存的引用指向的物件,下面直接稱為referent。
// GC特殊處理的物件
private T referent;         /* Treated specially by GC */
  • queue:Reference物件關聯的佇列,也就是引用佇列,物件如果即將被垃圾收集器回收,此佇列作為通知的回撥佇列,也就是當Reference例項持有的物件referent要被回收的時候,Reference例項會被放入引用佇列,那麼程式執行的時候可以從引用佇列得到或者監控相應的Reference例項。
    /* The queue this reference gets enqueued to by GC notification or by
     * calling enqueue().
     *
     * When registered: the queue with which this reference is registered.
     *        enqueued: ReferenceQueue.ENQUEUE
     *        dequeued: ReferenceQueue.NULL
     *    unregistered: ReferenceQueue.NULL
     */
    volatile ReferenceQueue<? super T> queue;
  • next:下一個Reference例項的引用,Reference例項通過此構造單向的連結串列。
    /* The link in a ReferenceQueue's list of Reference objects.
     *
     * When registered: null
     *        enqueued: next element in queue (or this if last)
     *        dequeued: this (marking FinalReferences as inactive)
     *    unregistered: null
     */
    @SuppressWarnings("rawtypes")
    volatile Reference next;
  • discovered:注意這個屬性由transient修飾,基於狀態表示不同連結串列中的下一個待處理的物件,主要是pending-reference列表的下一個元素,通過JVM直接呼叫賦值。
/* 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 */

例項方法(和ReferenceHandler執行緒不相關的方法):

// 獲取持有的referent例項
@HotSpotIntrinsicCandidate
public T get() {
     return this.referent;
}

// 把持有的referent例項置為null
public void clear() {
     this.referent = null;
}

// 判斷是否處於enqeued狀態
public boolean isEnqueued() {
     return (this.queue == ReferenceQueue.ENQUEUED);
}

// 入隊引數,同時會把referent置為null
public boolean enqueue() {
     this.referent = null;
     return this.queue.enqueue(this);
}

// 覆蓋clone方法並且丟擲異常,也就是禁止clone
@Override
protected Object clone() throws CloneNotSupportedException {
     throw new CloneNotSupportedException();
}

// 確保給定的引用例項是強可達的
@ForceInline
public static void reachabilityFence(Object ref) {
}

ReferenceHandler執行緒

ReferenceHandler執行緒是由Reference靜態程式碼塊中建立並且執行的執行緒,它的執行方法中依賴了比較多的本地(native)方法,ReferenceHandler執行緒的主要功能是處理pending連結串列中的引用物件:

    // 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 {
            // 確保Cleaner這個類已經初始化
            // pre-load and initialize Cleaner class so that we don't
            // get into trouble later in the run loop if there's
            // memory shortage while loading/initializing it lazily.
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, null, name, 0, false);
        }
        
        // 注意run方法是一個死迴圈執行processPendingReferences
        public void run() {
            while (true) {
                processPendingReferences();
            }
        }
    }

    /* 原子獲取(後)並且清理VM中的pending引用連結串列
     * Atomically get and clear (set to null) the VM's pending-Reference list.
     */
    private static native Reference<Object> getAndClearReferencePendingList();

    /* 檢驗VM中的pending引用物件連結串列是否有剩餘元素
     * Test whether the VM's pending-Reference list contains any entries.
     */
    private static native boolean hasReferencePendingList();

    /* 等待直到pending引用物件連結串列不為null,此方法阻塞的具體實現又VM實現
     * Wait until the VM's pending-Reference list may be non-null.
     */
    private static native void waitForReferencePendingList();

    // 鎖物件,用於控制等待pending物件時候的加鎖和開始處理這些物件時候的解鎖
    private static final Object processPendingLock = new Object();
    // 正在處理pending物件的時候,這個變數會更新為true,處理完畢或者初始化狀態為false,用於避免重複處理或者重複等待
    private static boolean processPendingActive = false;

    // 這個是死迴圈中的核心方法,功能是處理pending連結串列中的引用元素
    private static void processPendingReferences() {
        // Only the singleton reference processing thread calls
        // waitForReferencePendingList() and getAndClearReferencePendingList().
        // These are separate operations to avoid a race with other threads
        // that are calling waitForReferenceProcessing().
        // (1)等待
        waitForReferencePendingList();
        Reference<Object> pendingList;
        synchronized (processPendingLock) {
            // (2)獲取並清理,標記處理中狀態
            pendingList = getAndClearReferencePendingList();
            processPendingActive = true;
        }
        // (3)通過discovered(下一個元素)遍歷pending連結串列進行處理
        while (pendingList != null) {
            Reference<Object> ref = pendingList;
            pendingList = ref.discovered;
            ref.discovered = null;
            // 如果是Cleaner型別執行執行clean方法並且對鎖物件processPendingLock進行喚醒所有阻塞的執行緒
            if (ref instanceof Cleaner) {
                ((Cleaner)ref).clean();
                // Notify any waiters that progress has been made.
                // This improves latency for nio.Bits waiters, which
                // are the only important ones.
                synchronized (processPendingLock) {
                    processPendingLock.notifyAll();
                }
            } else {
                // 非Cleaner型別並且引用佇列不為ReferenceQueue.NULL則進行入隊操作
                ReferenceQueue<? super Object> q = ref.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(ref);
            }
        }
        // (4)當次迴圈結束之前再次喚醒鎖物件processPendingLock上阻塞的所有執行緒
        // Notify any waiters of completion of current round.
        synchronized (processPendingLock) {
            processPendingActive = false;
            processPendingLock.notifyAll();
        }
    }

ReferenceHandler執行緒啟動的靜態程式碼塊如下:

    static {
        // ThreadGroup繼承當前執行執行緒(一般是主執行緒)的執行緒組
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 建立執行緒例項,命名為Reference Handler,配置最高優先順序和後臺執行(守護執行緒),然後啟動
        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();
        // 注意這裡覆蓋了全域性的jdk.internal.misc.JavaLangRefAccess實現
        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean waitForReferenceProcessing()
                throws InterruptedException{
                return Reference.waitForReferenceProcessing();
            }

            @Override
            public void runFinalization() {
                Finalizer.runFinalization();
            }
        });
    }

    // 如果正在處理pending連結串列中的引用物件或者監測到VM中的pending連結串列中還有剩餘元素則基於鎖物件processPendingLock進行等待
    private static boolean waitForReferenceProcessing()
        throws InterruptedException{
        synchronized (processPendingLock) {
            if (processPendingActive || hasReferencePendingList()) {
                // Wait for progress, not necessarily completion.
                processPendingLock.wait();
                return true;
            } else {
                return false;
            }
        }
    }

由於ReferenceHandler執行緒是Reference的靜態程式碼建立的,所以只要Reference這個父類被初始化,該執行緒就會建立和執行,由於它是守護執行緒,除非JVM程序終結,否則它會一直在後臺執行(注意它的run()方法裡面使用了死迴圈)。

ReferenceQueue

JDK中對ReferenceQueue的文件描述是比較少的,類檔案只有一句簡單的註釋:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.

翻譯一下大意為:引用佇列,垃圾收集器在檢測到適當的可達性更改後將已註冊的引用物件追加到該佇列。

從原始碼上看,實際上ReferenceQueue只是名義上的引用佇列,它只儲存了Reference連結串列的頭(head)節點,並且提供了出隊、入隊和移除等操作,而Reference實際上本身提供單向連結串列的功能,也就是Reference通過成員屬性next構建單向連結串列,而連結串列的操作是委託給ReferenceQueue完成,這裡的邏輯有點繞。ReferenceQueue的原始碼比較少,這裡全量貼出標註一下注釋:

public class ReferenceQueue<T> {

    public ReferenceQueue() { }
    
    // 內部類Null類繼承自ReferenceQueue,覆蓋了enqueue方法返回false
    private static class Null extends ReferenceQueue<Object> {
        boolean enqueue(Reference<?> r) {
            return false;
        }
    }
    
    // ReferenceQueue.NULL和ReferenceQueue.ENQUEUED都是內部類Null的新例項
    static final ReferenceQueue<Object> NULL = new Null();
    static final ReferenceQueue<Object> ENQUEUED = new Null();
    
    // 靜態內部類,作為鎖物件
    private static class Lock { };
    // 鎖例項
    private final Lock lock = new Lock();
    // 引用連結串列的頭節點
    private volatile Reference<? extends T> head;
    // 引用佇列長度,入隊則增加1,出隊則減少1
    private long queueLength = 0;  

    // 入隊操作,只會被Reference例項呼叫
    boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
        // 加鎖
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            // 如果引用例項持有的佇列為ReferenceQueue.NULL或者ReferenceQueue.ENQUEUED則入隊失敗返回false
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            // Self-loop end, so if a FinalReference it remains inactive.
            // 如果連結串列沒有元素,則此引用例項直接作為頭節點,否則把前一個引用例項作為下一個節點
            r.next = (head == null) ? r : head;
            // 當前例項更新為頭節點,也就是每一個新入隊的引用例項都是作為頭節點,已有的引用例項會作為後繼節點
            head = r;
            // 佇列長度增加1
            queueLength++;
            // Update r.queue *after* adding to list, to avoid race
            // with concurrent enqueued checks and fast-path poll().
            // Volatiles ensure ordering.
            // 當前引用例項已經入隊,那麼它本身持有的引用佇列例項置為ReferenceQueue.ENQUEUED
            r.queue = ENQUEUED;
            // 特殊處理FinalReference,VM進行計數
            if (r instanceof FinalReference) {
                VM.addFinalRefCount(1);
            }
            // 喚醒所有等待的執行緒
            lock.notifyAll();
            return true;
        }
    }

    // 引用佇列的poll操作,此方法必須在加鎖情況下呼叫
    private Reference<? extends T> reallyPoll() {       /* Must hold lock */
        Reference<? extends T> r = head;
        if (r != null) {
            r.queue = NULL;
            // Update r.queue *before* removing from list, to avoid
            // race with concurrent enqueued checks and fast-path
            // poll().  Volatiles ensure ordering.
            @SuppressWarnings("unchecked")
            Reference<? extends T> rn = r.next;
            // Handle self-looped next as end of list designator.
            // 更新next節點為頭節點,如果next節點為自身,說明已經走過一次出隊,則返回null
            head = (rn == r) ? null : rn;
            // Self-loop next rather than setting to null, so if a
            // FinalReference it remains inactive.
            // 當前頭節點變更為環狀佇列,考慮到FinalReference尚為inactive和避免重複出隊的問題
            r.next = r;
            // 佇列長度減少1
            queueLength--;
            // 特殊處理FinalReference,VM進行計數
            if (r instanceof FinalReference) {
                VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

    // 佇列的公有poll操作,主要是加鎖後呼叫reallyPoll
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }
    
    // 移除引用佇列中的下一個引用元素,實際上也是依賴於reallyPoll的Object提供的阻塞機制
    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,超時時間為0,實際上就是lock.wait(0)就是永久阻塞直至喚醒
    public Reference<? extends T> remove() throws InterruptedException {
        return remove(0);
    } 

    // foreach
    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;
            }
        }
    }       
}    

ReferenceQueue的原始碼十分簡單,還是重新提一下,它只儲存了Reference連結串列的頭節點,真正的Reference連結串列的所有節點是儲存在Reference例項本身,通過屬性next拼接的,ReferenceQueue提供了對Reference連結串列的入隊、poll、remove等操作。

判斷物件的可達性和物件是否存活

判斷物件的可達性和物件是否存活是兩個比較困難的問題,筆者C語言學得比較爛,否則會重點翻看一下JVM的實現,目前只能參考一些資料來說明這個問題。

可達性演算法

主流商用語言包括Java都是使用可達性分析(Reachability Analysis)演算法來判定物件是否存活的。這個演算法的基本思路是通過一系列的稱為"GC Roots"(GC根集)的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC根集沒有任何引用鏈相連(從圖論的角度看,也就是從GC根集到這個物件是不可達的)時,則證明此物件是不可用的。不可用的物件"有機會"被判定為可以回收的物件。

在Java語言中,可以作為GC根集的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中常量引用的物件(在JDK1.8之後不存在方法區,也就是有可能是metaspace中常量引用的物件)。
  • 本地方法棧中JNI(即一般常說的Native方法)引用的物件。

finalize函式

即使在可達性分析演算法中判定為不可達的物件,也並非一定會判定為可以被回收的"死亡"物件。一個物件判定為"死亡"至少需要經歷兩次標記的過程。

第一次標記:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那麼它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。JVM會把以下兩種情況認為物件沒有必要執行finalize()方法:

  • 物件沒有覆蓋繼承自Object類的finalize()方法。
  • 物件的finalize()方法已經被JVM呼叫過。

如果一個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置在一個叫F-Queue的佇列之中,並且稍後由一個優先順序低的Finalizer執行緒去取該佇列的元素,"嘗試執行"元素的finalize()方法。這裡之所以叫嘗試執行是因為JVM會保證觸發滿足條件的物件的finalize()方法,但是並不承諾會等待它執行結束,這是因為:如果一個物件在執行finalize()方法耗時較長,甚至發生了死迴圈,將會導致F-Queue的佇列中的其他元素永遠處於等待狀態,極端情況下有可能導致整個記憶體回收系統崩潰。

finalize()方法是物件逃脫死亡命運的最後一次機會,因為稍後的GC將會對F-Queue佇列中的物件進行第二次小規模的標記,如果物件在finalize()方法執行過程中成功拯救自己--也就是物件自身重新與引用鏈的任何一個物件建立關聯即可,最常見的就是把自身(this關鍵字)賦值給某個類變數或者物件的成員屬性,那麼在第二次小規模的標記時候將會把"自我拯救"成功的物件移出"即將回收"的集合。如果物件在finalize()方法執行過程中沒有"逃逸",那麼它最終就會被回收。參考《深入理解Java虛擬機器-2nd》的"物件自我拯救的例子":

public class FinalizeEscapeGc {

    private static FinalizeEscapeGc SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("Yes,I am still alive :)");
    }

    public static void main(String[] args) throws Exception {
        SAVE_HOOK = new FinalizeEscapeGc();

        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("No,I am not alive :(");
        }
                // 下面的這段程式碼和上面的一致
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("No,I am not alive :(");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("FinalizeEscapeGc finalize invoke...");
        FinalizeEscapeGc.SAVE_HOOK = this;
    }
}
// 輸出結果
FinalizeEscapeGc finalize invoke...
Yes,I am still alive :)
No,I am not alive :(

注意:

  • finalize()方法的錯誤使用有可能是記憶體回收系統崩潰的根源,一般情況下謹慎思考是否真的需要覆蓋此方法。
  • 任意一個物件只能通過finalize()方法自我拯救一次。

Finalizer守護執行緒

前面提到的Finalizer守護執行緒和F-Queue佇列其實在JDK中有具體的實現類java.lang.ref.FinalizerF-Queue佇列只是《深入理解Java虛擬機器-2nd》中的一個名詞描述,實際上筆者沒有找到相關的資料,這裡我們通過分析JDK和JVM相關的原始碼去理解這個F-Queue佇列吧。先看java.lang.ref.Finalizer的原始碼,程式碼比較少全量貼出:

final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
                                                          same package as the Reference
                                                          class */
    // Finalizer關聯的ReferenceQueue,其實Finalizer是一個特殊的Reference實現
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

    /** Head of doubly linked list of Finalizers awaiting finalization. */
    // 等待finalization的所有Finalizer例項連結串列的頭節點,這裡稱此連結串列為unfinalized連結串列
    private static Finalizer unfinalized = null;

    /** Lock guarding access to unfinalized list. */
    // unfinalized連結串列的鎖,靜態final,全域性的鎖例項
    private static final Object lock = new Object();
    
    // 中間變數,分別記錄unfinalized連結串列中當前執行元素的下一個節點和前一個節點
    private Finalizer next, prev;

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        // push onto unfinalized
        // 這裡主要是更新unfinalized連結串列的頭節點,新增的元素總是會變成頭節點
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }

    static ReferenceQueue<Object> getQueue() {
        return queue;
    }

    /* Invoked by VM */ 這個方法由JVM啟用,也就是連結串列的元素入隊是由JVM控制的,見下文分析
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (lock) {
            // 當前元素已經處理過,直接返回
            if (this.next == this)      // already finalized
                return;
            // unlink from unfinalized
            // 下面的邏輯是當前需要執行的元素從連結串列中移除,並且更新prev和next的值,相當於重建連結串列的部分節點
            if (unfinalized == this)
                unfinalized = this.next;
            else
                this.prev.next = this.next;
            if (this.next != null)
                this.next.prev = this.prev;
            this.prev = null;
            this.next = this;           // mark as finalized
        }

        try {
            // 獲取物件執行一次finalize方法
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                // Clear stack slot containing this variable, to decrease
                // the chances of false retention with a conservative GC
                // 清空變數引用從而減少保守GC導致變數保留的可能性
                finalizee = null;
            }
        } catch (Throwable x) { }
        // 執行完畢會做一次情況防止重複執行
        super.clear();
    }

    /* Create a privileged secondary finalizer thread in the system thread
     * group for the given Runnable, and wait for it to complete.
     *
     * This method is used by runFinalization.
     *
     * It could have been implemented by offloading the work to the
     * regular finalizer thread and waiting for that thread to finish.
     * The advantage of creating a fresh thread, however, is that it insulates
     * invokers of that method from a stalled or deadlocked finalizer thread.
     */
    // 這裡其實不用畏懼註釋太多,它只是一個候選方法,新建一個執行緒直接呼叫包裹在Runnable的runFinalization方法,主要是提供給主動呼叫的上層方法呼叫的
    private static void forkSecondaryFinalizer(final Runnable proc) {
        AccessController.doPrivileged(
            new PrivilegedAction<>() {
                public Void run() {
                    ThreadGroup tg = Thread.currentThread().getThreadGroup();
                    for (ThreadGroup tgn = tg;
                         tgn != null;
                         tg = tgn, tgn = tg.getParent());
                    Thread sft = new Thread(tg, proc, "Secondary finalizer", 0, false);
                    sft.start();
                    try {
                        sft.join();
                    } catch (InterruptedException x) {
                        Thread.currentThread().interrupt();
                    }
                    return null;
                }});
    }

    /* Called by Runtime.runFinalization() */
    // 這個方法是給Runtime.runFinalization()委託呼叫的,其實就是主動取出queue的元素強制呼叫其finalize方法
    static void runFinalization() {
        if (VM.initLevel() == 0) {
            return;
        }
        forkSecondaryFinalizer(new Runnable() {
            private volatile boolean running;
            public void run() {
                // in case of recursive call to run()
                if (running)
                    return;
                final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                running = true;
                for (Finalizer f; (f = (Finalizer)queue.poll()) != null;)
                    f.runFinalizer(jla);
            }
        });
    }
    
    // 真正的Finalizer執行緒
    private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, null, "Finalizer", 0, false);
        }
        public void run() {
            // in case of recursive call to run()
            if (running)
                return;

            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (VM.initLevel() == 0) {
                // delay until VM completes initialization
                try {
                    VM.awaitInitLevel(1);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            // 注意這裡是死迴圈
            for (;;) {
                try {
                    // 注意這裡是呼叫`Reference#remove()`的永久阻塞版本,只有`Reference#enqueue()`被呼叫才會解除阻塞
                    // `Reference#remove()`解除阻塞說明元素已經完成入隊,由ReferenceHandler執行緒完成
                    Finalizer f = (Finalizer)queue.remove();
                    // 實際上就是呼叫物件的finalize方法
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
        }
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 靜態程式碼塊中宣告執行緒,優先順序是最高優先順序-2,守護執行緒,實際上這裡優先順序不一定會生效
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }
}

上面的註釋已經很明顯標註出來,這裡小結一下內容。

  • FinalizerFinalReference的子類,而FinalReferenceReference的實現,所以它的工作原理和其他引用類似,物件的狀態更變和由ReferenceHandler執行緒密切相關。
  • Finalizer內部維護了一個連結串列,每當JVM呼叫靜態註冊方法就會新建一個Finalizer例項加入到連結串列的頭節點中,頭節點元素為unfinalized,這裡稱此連結串列為unfinalized連結串列。
  • Finalizer執行緒由Finalizer靜態程式碼塊構建並且執行,它是守護執行緒,優先順序是最高優先順序-2,它的作用就是提取unfinalized連結串列的元素並且執行元素物件的finalize()方法,過程中還會涉及到執行緒的阻塞、喚醒,以及unfinalized連結串列的重建等工作。

由於靜態方法Finalizer#register(Object finalizee)是由JVM呼叫的,所以我們必須要分析一些JVM的原始碼,參考的是OpenJDK主分支的程式碼,檔案是instanceKlass.cpp

instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) {
  if (TraceFinalizerRegistration) {
    tty->print("Registered ");
    i->print_value_on(tty);
    tty->print_cr(" (" INTPTR_FORMAT ") as finalizable", p2i(i));
  }
  instanceHandle h_i(THREAD, i);
  // Pass the handle as argument, JavaCalls::call expects oop as jobjects
  JavaValue result(T_VOID);
  JavaCallArguments args(h_i);
  // 這裡Universe::finalizer_register_method()獲取到的就是Finalizer#register方法控制代碼
  methodHandle mh (THREAD, Universe::finalizer_register_method());
  JavaCalls::call(&result, mh, &args, CHECK_NULL);
  return h_i();
}

最後呼叫的是javaCalls.cpp

void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
  // Check if we need to wrap a potential OS exception handler around thread
  // This is used for e.g. Win32 structured exception handlers
  assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
  // Need to wrap each and every time, since there might be native code down the
  // stack that has installed its own exception handlers
  os::os_exception_wrapper(call_helper, result, method, args, THREAD);
}

簡單來看就是把建立物件過程中,如果有必要註冊Finalizer(一般是覆蓋了finalize()方法),則基於當前執行緒通過Finalizer#register(Object finalizee)把當前新建的例項註冊到Finalizer自身維護的連結串列中(如果沒理解錯,所謂的F-Queue就是這個連結串列了),等待後臺Finalizer執行緒輪詢並且執行連結串列中物件的finalize()方法。

各類引用以及它們的使用場景

這裡提到的各類引用目前就是四種:強引用(StrongReference)、軟引用(SoftReference)、弱引用(WeakReference)和虛引用(PhantomReference)。其實還有特殊的引用型別FinalReference,它是包私有的,並且只有一個子型別Finalizer

StrongReference

StrongReference也就是強引用,它是使用最普遍的一種引用,java.lang.ref包下沒有強引用對應的型別。一個比較明確的強引用定義就是:所有和GC Root之間存在引用鏈的物件都具備強引用。舉個簡單例子:形如Object o = new Object();在方法體中使用new關鍵字宣告的物件一般就是強引用。如果一個物件具備強引用,垃圾回收器絕不會回收它。當記憶體空間不足,JVM寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會出現回收具有強引用的物件來解決記憶體不足的情況。當然,如果有共享的成員變數在方法退出之前置為null,相當於斷絕成員變數和GC Root的引用鏈,在合適的時機是有利於GC後具備強引用的物件的回收,例如:

private Object shareValue = XXX;

public void methodA(){
    //do something
    shareValue = null;
}

後來有人過度信奉類似上面的這個實踐,出現了一條比較詭異的編碼實踐:強引用使用完畢後都要置為null方便物件回收。但是實際上,這個實踐並不是在任何場景都是合理的。

SoftReference

SoftReference也就是軟引用,它是用來描述一些"還有用但是非必須"的物件。對於軟引用關聯著的物件,在JVM應用即將發生記憶體溢位異常之前,將會把這些軟引用關聯的物件列進去回收物件範圍之中進行第二次回收。如果這次回收之後還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。簡單來說就是:

  • 如果記憶體空間足夠,垃圾回收器就不會回收軟引用關聯著的物件。
  • 如果記憶體空間不足,垃圾回收器在將要丟擲記憶體溢位異常之前會回收軟引用關聯著的物件。

舉個簡單的使用例子:

// VM引數:-Xmx4m -Xms4m
public class SoftReferenceMain {

    public static void main(String[] args) throws Exception {
        ReferenceQueue<SoftReferenceObject> queue = new ReferenceQueue<>();
        SoftReferenceObject object = new SoftReferenceObject();
        SoftReference<SoftReferenceObject> reference = new SoftReference<>(object, queue);
        object = null;
        System.gc();
        Thread.sleep(500);
        System.out.println(reference.get());
    }

    private static class SoftReferenceObject {

        int[] array = new int[120_000];

        @Override
        public String toString() {
            return "SoftReferenceObject";
        }
    }
}
// 執行後輸出結果
null

上面的例子故意把JVM的啟動的最大Heap記憶體和初始Heap記憶體設定為4MB,使用這個物件初始化一個比較大的整型陣列並且關係到一個軟引用物件中,GC之後,發現軟引用關聯的物件被回收了。

WeakReference

WeakReference也就是弱引用,弱引用和軟引用類似,它是用來描述"非必須"的物件的,它的強度比軟引用要更弱一些。被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,簡言之就是:一旦發生GC必定回收被弱引用關聯的物件,不管當前的記憶體是否足夠。

舉個例子:

public class WeakReferenceMain {

    public static void main(String[] args) throws Exception {
        ReferenceQueue<WeakReferenceObject> queue = new ReferenceQueue<>();
        WeakReferenceObject object = new WeakReferenceObject();
        System.out.println(object);
        WeakReference<WeakReferenceObject> reference = new WeakReference<>(object, queue);
        object = null;
        System.gc();
        Thread.sleep(500);
        System.out.println(reference.get());
    }

    private static class WeakReferenceObject {

        @Override
        public String toString() {
            return "WeakReferenceObject";
        }
    }
}
// 執行後輸出結果
WeakReferenceObject
null

上面的例子中沒有設定JVM的堆記憶體,因此不存在記憶體不足的情況,可見弱引用關聯的物件在GC之後被回收了。弱引用適合用來做對記憶體敏感的快取,很常用的WeakHashMap就是基於弱引用實現的。

PhantomReference

PhantomReference也就是虛引用,也叫幽靈引用或者幻影引用,它是所有引用型別中最弱的一種。一個物件是否關聯到虛引用,完全不會影響該物件的生命週期,也無法通過虛引用來獲取一個物件的例項(PhantomReference覆蓋了Reference#get()並且總是返回null)。為物件設定一個虛引用的唯一目的是:能在此物件被垃圾收集器回收的時候收到一個系統通知。PhantomReference有兩個比較常用的子類是java.lang.ref.Cleanerjdk.internal.ref.Cleaner,其中前者提供的功能是開發者用於在引用物件回收的時候觸發一個動作(java.lang.ref.Cleaner$Cleanable),後者用於DirectByteBuffer物件回收的時候對於堆外記憶體的回收,可以翻看前面描述java.lang.ref.Reference#processPendingReferences()原始碼的時候,ReferenceHandler執行緒會對pending連結串列中的jdk.internal.ref.Cleaner型別引用物件呼叫其clean()方法。PhantomReference本身使用場景比較少,這裡舉一下java.lang.ref.Cleaner註釋中的例子:

public class PhantomReferenceMain {

    public static void main(String[] args) throws Exception {
        try (CleaningExample o = new CleaningExample(11)){

        }
        CleaningExample o2 = new CleaningExample(22);
        System.gc();
        Thread.sleep(300);
    }

}

class CleaningExample implements AutoCloseable {

    private Cleaner cleaner = Cleaner.create();
    private final State state;
    private final Cleaner.Cleanable cleanable;

    public CleaningExample(int s) {
        state = new State(s);
        cleanable = cleaner.register(this, state);
    }

    class State implements Runnable {

        private final int s;

        public State(int s) {
            this.s = s;
        }

        @Override
        public void run() {
            System.out.println("State runnable in action.State value = " + s);
        }
    }

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

實際上,沙面的程式碼執行完畢只會輸出"State runnable in action.State value = 11",並沒有輸出"State runnable in action.State value = 22",這是因為無法預測強引用物件被回收的時機。java.lang.ref.Cleaner主要是用於預防實現了AutoCloseable介面的例項忘記呼叫close()方法在物件被垃圾收集器回收的時候(記憶體回收)做一個兜底的清理工作,在JDK9之後,java.lang.ref.Cleaner主要是為了替代已經標識為過期的Object#finalize()方法。

擴充套件閱讀:可以注意閱讀一下《Effective Java 3rd》的第8小節,摘抄部分內容如下:終結方法(Finalizer)是不可預知的,很多時候是危險的,而且一般情況下是不必要的。...在Java 9中,終結方法已經被遺棄了,但它們仍被Java類庫使用,相應用來替代終結方法的是清理方法(cleaner)。比起終結方法,清理方法相對安全點,但仍是不可以預知的,執行慢的,而且一般情況下是不必要的。

JDK9中有很多原來使用覆蓋Object#finalize()方法的清理工作實現都替換為java.lang.ref.Cleaner,但是仍然不鼓勵使用這種方式。

Reference和ReferenceQueue配合使用

前面基本介紹完了所有型別引用以及相關的原始碼,但是尚未提供例子說明ReferenceReferenceQueue是怎麼配合使用的。舉個例子:

public class ReferenceQueueMain {

    public static void main(String[] args) throws Exception {
        ReferenceQueue<WeakReferenceObject> queue = new ReferenceQueue<>();
        WeakReferenceObject object = new WeakReferenceObject();
        WeakReference<WeakReferenceObject> weakReference = new WeakReference<>(object, queue);
        System.out.println(weakReference);
        object = null;
        System.gc();
        Thread.sleep(500);
        while (true) {
            Reference<? extends WeakReferenceObject> reference = queue.poll();
            if (null == reference) {
                Thread.sleep(100);
            } else {
                System.out.println(reference);
                System.out.println(reference.get());
                break;
            }
        }
    }

    private static class WeakReferenceObject {

        @Override
        public String toString() {
            return "WeakReferenceObject";
        }
    }
}

執行後輸出結果是:

java.lang.ref.WeakReference@6537cf78
java.lang.ref.WeakReference@6537cf78
null

可見輪詢ReferenceQueue例項得到的弱引用例項和建立的是一致的,只是它持有的關聯的物件已經被回收,得到null。上面的ReferenceQueue#poll()方法也可以替換為ReferenceQueue#remove(),這樣子就不用寫在死迴圈中,因為ReferenceQueue#remove()會阻塞到有元素可以出隊。通過輪詢繫結到Reference例項的ReferenceQueue例項,就可以得知Reference例項當前的狀態並且判斷它關聯的我們真正關注的物件是否被回收。

小結

  • Reference是非強引用的其他三種引用的共同父類。
  • ReferenceQueue只儲存了引用連結串列的頭節點,提供了引用連結串列的操作,實際上,引用連結串列是Reference例項內部變數儲存的。
  • ReferenceHandler守護執行緒執行緒由Reference的靜態程式碼塊建立和執行,作用是處理pending連結串列的引用元素使之狀態變更,伴隨著ReferenceQueue的相關操作。
  • Finalizer守護執行緒是由Finalizer類的靜態程式碼塊建立和執行的,作用是處理Finalizer類內部維護的F-Queue連結串列(連結串列元素入隊操作由JVM實現)的元素呼叫關聯物件的finalize()方法。
  • ReferenceHandler守護執行緒線和Finalizer守護執行緒共同協作才能使引用型別物件記憶體回收系統的工作能夠正常進行。

四種引用型別的總結:

引用型別 被垃圾收集器回收的時機 主要用途 生存週期
強引用 直到記憶體溢位也不會回收 普遍物件的狀態 從建立到JVM例項終止執行
軟引用 垃圾回收並且記憶體不足時 有用但非必須的物件快取 從建立到垃圾回收並且記憶體不足時
弱引用 垃圾回收時 非必須的物件快取 上一次垃圾回收結束到下一次垃圾回收開始
虛引用 - 關聯的物件被垃圾收集器回收時候得到一個系統通知 -

參考資料:

  • JDK11部分原始碼。
  • 《深入理解Java虛擬機器-2nd》- 這本書算是國內書籍寫得比較良心的一本了,不過有很多小的問題或者筆誤之處,需要自行發現和修正。

個人部落格

  • Throwable's Blog

(過年比較懶,很久沒發文 e-a-20190215 c-14