1. 程式人生 > >你不可不知的Java引用型別之——Reference原始碼解析

你不可不知的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