你不可不知的Java引用類型之——Reference源碼解析


定義

Reference是所有引用類型的父類,定義了引用的公共行為和操作。

reference指代引用對象本身,referent指代reference引用的對象,下文介紹會以reference,referent形式出現。

說明

Reference類與垃圾回收是密切配合的,所以該類不能被直接子類化。簡單來講,Reference的繼承類都是經過嚴格設計的,甚至連成員變量的先后順序都不能改變,所以在代碼中直接繼承Reference類是沒有任何意義的。但是可以繼承Reference類的子類。

例如:Finalizer 繼承自 FinalReference,Cleaner 繼承自 PhantomReference

構造函數

Reference類中有兩個構造函數,一個需要傳入引用隊列,另一個則不需要。

這個隊列的意義在於增加一種判斷機制,可以在外部通過監控這個隊列來判斷對象是否被回收。如果一個對象即將被回收,那么引用這個對象的reference對象就會被放到這個隊列中。通過監控這個隊列,就可以取出這個reference后再進行一些善后處理。

如果沒有這個隊列,就只能通過不斷地輪詢reference對象,通過get方法是否返回null( phantomReference對象不能這樣做,其get方法始終返回null,因此它只有帶queue的構造函數 )來判斷對象是否被回收。

這兩種方法均有相應的使用場景,具體使用需要具體情況具體分析。比如在weakHashMap中,就通過查詢queue的數據,來判定是否有對象將被回收。而ThreadLocalMap,則采用判斷get()是否為null來進行處理。

/* -- Constructors -- */
Reference(T referent) {
    this(referent, null);
}

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

內部成員

Reference類內部有這么幾個成員變量:

referent:保存reference指向的對象。

private T referent;

queue:引用對象關聯的引用隊列。是對象即將被回收時所要通知的隊列。當對象將被回收時,reference對象( 而不是referent引用的對象 )會被放到queue里面,然后外部程序即可通過監控這個queue拿到相應的數據了。

這里的queue( 即,ReferenceQueue對象 )名義上是一個隊列,實際內部是使用單鏈表來表示的單向隊列,可以理解為queue就是一個鏈表,其自身僅存儲當前的head節點,后面的節點由每個reference節點通過next來保持即可。

volatile ReferenceQueue<? super T> queue;

next:指向下一個引用,Reference是一個單鏈表的結構。

Reference next;

discovered:表示要處理的對象的下一個對象。

/* 當處於active狀態: discovered鏈表中下一個待處理對象
 * 當處於pending狀態: pending列表中的下一個對象
 * 其它狀態:   NULL
 */
transient private Reference<T> discovered;

lock:內部同步鎖對象。用作在操作pending鏈表時的同步對象。注意這是一個靜態對象,意味着所有Reference對象共用同一個鎖。

static private class Lock { }
private static Lock lock = new Lock();

pending:等待添加到queue中的元素鏈表。注意這是一個靜態對象,意味着所有Reference對象共用同一個pending隊列。

/* 用來保存那些需要被放入隊列中的reference,收集器會把引用添加到這個列表里來,
 * Reference-handler線程會從中移除它們。
 * 這個列表由上面的lock對象鎖進行保護。列表使用discovered字段來鏈接它的元素。
 */
private static Reference<Object> pending = null;

::: warning 說明
queue隊列使用next來查找下一個reference,pending隊列使用discovered來查找下一個reference。
:::

Reference狀態

在Reference類中,有一段很長的注釋,來對內部對象referent的狀態進行了說明。

Active:
reference如果處於此狀態,會受到垃圾處理器的特殊處理。當垃圾回收器檢測到referent已經更改為合適的狀態后(沒有任何強引用和軟引用關聯),會在某個時間將實例的狀態更改為Pending或者Inactive。具體取決於實例是否在創建時注冊到一個引用隊列中。
在前一種情況下(將狀態更改為Pending),他還會將實例添加到pending-Reference列表中。新創建的實例處於活動狀態。

Pending:
實例如果處於此狀態,表明它是pending-Reference列表中的一個元素,等待被Reference-handler線程做入隊處理。未注冊引用隊列的實例永遠不會處於該狀態。

Enqueued:
實例如果處於此狀態,表明它已經是它注冊的引用隊列中的一個元素,當它被從引用隊列中移除時,它的狀態將會變為Inactive,未注冊引用隊列的實例永遠不會處於該狀態。

Inactive:
實例如果處於此狀態,那么它就是個廢實例了(滑稽),它的狀態將永遠不會再改變了。

所以實例一共有四種狀態,Active(活躍狀態)、Pending(半死不活狀態)、Enqueued(瀕死狀態)、Inactive(涼涼狀態)。當然,Pending和Enqueued狀態是引用實例在創建時注冊了引用隊列才會有。

一個reference處於Active狀態時,表示它是活躍正常的,垃圾回收器會監視這個引用的referent,如果掃描到它沒有任何強引用關聯時就會進行回收判定了。

如果判定為需要進行回收,則判斷其是否注冊了引用隊列,如果有的話將reference的狀態置為pending。當reference處於pending狀態時,表明已經准備將它放入引用隊列中,在這個狀態下要處理的對象將逐個放入queue中。在這個時間窗口期,相應的引用對象為pending狀態。

當它進入到Enqueued狀態時,表明已經引用實例已經被放到queue當中了,准備由外部線程來輪詢獲取相應信息。此時引用指向的對即將被垃圾回收器回收掉了。

當它變成Inactive狀態時,表明它已經涼透了,它的生命已經到了盡頭。不管你用什么方式,也救不了它了。

JVM中並沒有顯示定義這樣的狀態,而是通過next和queue來進行判斷。

Active:如果創建Reference對象時,沒有傳入ReferenceQueue,queue=ReferenceQueue.NULL。如果有傳入,則queue指向傳入的ReferenceQueue隊列對象。next == null;

Pending:queue為初始化時傳入ReferenceQueue對象;next == this;

Enqueue:queue == ReferenceQueue.ENQUEUED;next為queue中下一個reference對象,或者若為最后一個了next == this;

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

如果next==null,則reference處於Active狀態;

如果next!=null,queue == ReferenceQueue.NULL,則reference處於Inactive狀態;

如果next!=null,queue == ReferenceQueue.ENQUEUED,則reference處於Enqueue狀態;

如果next != null,queue != ReferenceQueue.NULL && queu != ReferenceQueue.ENQUEUED ,則reference處於Pending狀態。

ReferenceHandler線程

Reference類中有一個特殊的線程叫ReferenceHandler,專門處理那些pending鏈表中的引用對象。ReferenceHandler類是Reference類的一個靜態內部類,繼承自Thread,所以這條線程就叫它ReferenceHandler線程。

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 {
        // 預加載並初始化 InterruptedException 和 Cleaner 類
        // 來避免出現在循環運行過程中時由於內存不足而無法加載它們		 
        ensureClassInitialized(InterruptedException.class);
        ensureClassInitialized(Cleaner.class);
    }

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

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

這個類其實也很簡單,就是先預加載了兩個類,然后run方法中使用了while死循環運行tryHandlerPending方法。這個方法通過名字就能大概判斷,應該是來處理pending鏈表的,讓我們看看它的內部代碼:

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    // 將handler線程注冊到根線程組中並設置最高優先級
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();

    // 覆蓋jvm的默認處理方式
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

這里其實就是在靜態代碼段里在根線程組中啟動了一條最高優先級的ReferenceHandler線程,並覆蓋了JVM中對pending的默認處理方式。嗯,關鍵點就在 tryHandlePending(false) 這一句了。接下來再看看這里的實現:

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            // 如果pending鏈表不為null,則開始進行處理
            if (pending != null) {
                r = pending;
                // 使用 'instanceof' 有時會導致OOM
                // 所以在將r從鏈表中摘除時先進行這個操作
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // 移除頭結點,將pending指向其后一個節點
                pending = r.discovered;
                // 此時r為原來pending鏈表的頭結點,已經從鏈表中脫離出來
                r.discovered = null;
            } else {
                // 在鎖上等待可能會造成OOM,因為它會試圖分配exception對象
                if (waitForNotify) {
                    lock.wait();
                }
                // 重試
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        // 重試
        return true;
    } catch (InterruptedException x) {
        // 重試
        return true;
    }

    // 如果摘除的元素是Cleaner類型,則執行其clean方法
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;
    // 最后,如果其引用隊列不為空,則將該元素入隊
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

所以,這里整個過程就是摘取pending鏈表的頭結點,如果是Cleaner,則執行clean操作,否則進行入隊處理。

常用方法

/**
  * 返回引用指向的對象,如果referent已經被程序或者垃圾回收器清理,則返回null。
  */
public T get() {
    return this.referent;
}

/**
  * 清理referent對象,調用該方法不會使得這個對象進入Enqueued狀態。
  */
public void clear() {
    this.referent = null;
}

/**
  * 判斷該reference是否已經入隊。
  */
public boolean isEnqueued() {
    return (this.queue == ReferenceQueue.ENQUEUED);
}

/**
  * 將該引用添加到其注冊的引用隊列中。
  * 如果reference成功入隊則返回true,如果它已經在隊列中或者創建時沒有注冊隊列則返回false
  */
public boolean enqueue() {
    return this.queue.enqueue(this);
}

Reference類就是用來包裝對象的,通過跟JVM的一些密切配合,使得被包裹其中的對象能夠被JVM特殊處理,所以使用Reference對象可以使得我們在更細粒度上控制對象的生命周期。

小結

  • Reference類是所有引用類的父類

  • Reference中可以在創建時注冊引用隊列

  • Reference有四種狀態,如果創建時沒有注冊引用隊列,則只有兩種狀態

  • 可以通過get方法獲取內部的對象,但如果對象已經被回收了,則會返回null


免責聲明!

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



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