深入理解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》- 這本書算是國內書籍寫得比較良心的一本了,不過有很多小的問題或者筆誤之處,需要自行發現和修正。

個人博客

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

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM