Java源碼剖析——徹底搞懂Reference和ReferenceQueue

之前博主的一篇讀書筆記——《深入理解Java虛擬機》系列之回收對象算法與四種引用類型博客中爲大家介紹了Java中的四種引用類型,很多同學都希望能夠對引用,還有不同類型引用的原理進行更深入的瞭解。因此博主查看了抽象父類Reference和負責註冊引用對象的引用隊列ReferenceQueue的源碼,在此和大家一起分享,並做了一些分析,感興趣的同學可以一起學習。

Reference源碼分析

首先我們先看一下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(T referent) {
    this(referent, null);
}

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

一個構造函數帶需要註冊到的引用隊列,一個不帶。帶queue的意義在於我們可以吃從外部通過對queue的操作來瞭解到引用實例所指向的實際對象是否被回收了,同時我們也可以通過queue對引用實例進行一些額外的操作;但如果我們的引用實例在創建時沒有指定一個引用隊列,那我們要想知道實際對象是否被回收,就只能夠不停地輪詢引用實例的get()方法是否爲空了。值得注意的是虛引用PhantomReference,由於它的get()方法永遠返回null,因此它的構造函數必須指定一個引用隊列。這兩種查詢實際對象是否被回收的方法都有應用,如weakHashMap中就選擇去查詢queue的數據,來判定是否有對象將被回收;而ThreadLocalMap,則採用判斷get()是否爲null來作處理。

接下來是它的主要成員:

private T referent;         /* Treated specially by GC */

在這裏我們首先明確一些名詞,Reference類也被稱爲引用類,它的實例 Reference Instance就是引用實例,但是由於它是一個抽象類,它的實例只能是子類軟(soft)引用,弱(weak)引用,虛(phantom)引用中的某個,至於引用實例所引用的對象我們稱之爲實際對象(也就是我們上面所寫出的referent)。

volatile ReferenceQueue<? super T> queue;   /* 引用對象隊列*/

queue是當前引用實例所註冊的引用隊列,一旦實際對象的可達性發生適當的變化後,此引用實例將會被添加到queue中。

/* When active:   NULL
 *     pending:   this
 *    Enqueued:   next reference in queue (or this if last)
 *    Inactive:   this
 */
@SuppressWarnings("rawtypes")
Reference next;

next用來表示當前引用實例的下一個需要被處理的引用實例,我們在註釋中看到的四個狀態,是引用實例的內部狀態,不可以被外部查看或是直接修改:

  • Active:新創建的引用實例處於Active狀態,但當GC檢測到該實例引用的實際對象的可達性發生某些適當的改變(實際對象對於GC roots不可達)後,它的狀態將會根據此實例是否註冊在引用隊列中而變成Pending或是Inactive。
  • Pending:當引用實例被放置在pending-Reference list中時,它處於Pending狀態。此時,該實例在等待一個叫Reference-handler的線程將此實例進行enqueue操作。如果某個引用實例沒有註冊在一個引用隊列中,該實例將永遠不會進入Pending狀態。
  • Enqueued: 當引用實例被添加到它註冊在的引用隊列中時,該實例處於Enqueued狀態。當某個引用實例被從引用隊列中刪除後,該實例將從Enqueued狀態變爲Inactive狀態。如果某個引用實例沒有註冊在一個引用隊列中,該實例將永遠不會進入Enqueued狀態。
  • Inactive:一旦某個引用實例處於Inactive狀態,它的狀態將不再會發生改變,同時說明該引用實例所指向的實際對象一定會被GC所回收。

事實上Reference類並沒有顯示地定義內部狀態值,JVM僅需要通過成員queue和next的值就可以判斷當前引用實例處於哪個狀態:

  • Active:queue爲創建引用實例時傳入的ReferenceQueue的實例或是ReferenceQueue.NULL;next爲null
  • Pending:queue爲創建引用實例時傳入的ReferenceQueue的實例;next爲this
  • Enqueued:queue爲ReferenceQueue.ENQUEUED;next爲隊列中下一個需要被處理的實例或是this如果該實例爲隊列中的最後一個
  • Inactive:queue爲ReferenceQueue.NULL;next爲this
/* 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;

/* 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 */

看到註釋的同學們有可能會有一些疑惑,明明pending是一個Reference類型的對象,爲什麼註釋說它是一個list呢?其實是因爲GC檢測到某個引用實例指向的實際對象不可達後,會將該pending指向該引用實例,discovered字段則是用來表示下一個需要被處理的實例,因此我們只要不斷地在處理完當前pending之後,將discovered指向的實例賦予給pending即可。所以這個static字段pending其實就是一個鏈表。

private static class ReferenceHandler extends Thread {
  ......
  public void run() {
      while (true) {
          tryHandlePending(true);
      }
  }
}

ReferenceHandler是一個優先級最高的線程,它執行的工作就是將pending list中的引用實例添加到引用隊列中,並將pending指向下一個引用實例。

 static boolean tryHandlePending(boolean waitForNotify) {
     ......
     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;
        }
    }
    ......
    ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
 }

Reference對外提供的方法就比較簡單了:

public T get() {
   return this.referent;
}

get()方法就是簡單的返回引用實例所引用的實際對象,如果該對象被回收了或者該引用實例被clear了則返回null

public void clear() {
  this.referent = null;
}

調用此方法不會導致此對象入隊。此方法僅由Java代碼調用;當垃圾收集器清除引用時,它直接執行,而不調用此方法。
clear的方法本質上就是將referent置爲null,清除引用實例所引用的實際對象,這樣通過get()方法就不能再訪問到實際對象了。

public boolean isEnqueued() {
  return (this.queue == ReferenceQueue.ENQUEUED);
}

判斷此引用實例是否已經被放入隊列中是通過引用隊列實例是否等於ReferenceQueue.ENQUEUED來得知的。

public boolean enqueue() {
  return this.queue.enqueue(this);
}

enqueue()方法能夠手動將引用實例加入到引用隊列當中去。

ReferenceQueue源碼分析

同樣我們先看一下ReferenceQueue的註釋:

/**
 * Reference queues, to which registered reference objects are appended by the
 * garbage collector after the appropriate reachability changes are detected.
 * 引用隊列,在檢測到適當的可到達性更改後,垃圾回收器將已註冊的引用對象添加到該隊列中
 */

ReferenceQueue實現了隊列的入隊(enqueue)和出隊(poll),其中的內部元素就是我們上文中提到的Reference對象。隊列元素的存儲結構是單鏈式存儲,依靠每個reference對象的next域去找下一個元素。

主要成員有:

private  volatile Reference extends T> head = null;

用來存儲當前需要被處理的節點

static ReferenceQueue NULL = new Null<>();
static ReferenceQueue ENQUEUED = new Null<>();

static變量NUlL和ENQUEUED分別用來表示沒有提供默認引用隊列的空隊列和已經執行過enqueue操作的隊列。

引用實例入隊的邏輯很簡單:

synchronized (lock) {
    // 檢查reference是否已經執行過入隊操作
    ReferenceQueue<?> queue = r.queue;
    if ((queue == NULL) || (queue == ENQUEUED)) {
        return false;
    }
    //將引用實例的成員queue置爲ENQUEUED
    r.queue = ENQUEUED;
    //若頭節點爲空,說明該引用實例爲隊列中的第一個元素,將它的next實例等於this
    //若頭節點不爲空,將它的next實例指向頭節點指向的元素
    r.next = (head == null) ? r : head;
    //頭節點指向當前引用實例
    head = r;
    //length+1
    queueLength++;

    lock.notifyAll();
    return true;
}

簡單來說,入隊操作就是將每次需要入隊的引用實例放在頭節點的位置,並將它的next域指向舊的頭節點元素。因此整個ReferenceQueue是一個後進先出的數據結構。

出隊的邏輯爲:

r指向頭節點元素
Reference<? extends T> r = head;
if (r != null) {
    //頭節點指向null,如果隊列中只有一個元素;否則指向r.next
    head = (r.next == r) ? null : r.next; 
    //頭節點元素的queue指向ReferenceQueue.NULL
    r.queue = NULL;
    //將r.next指向this
    r.next = r;
    //length-1
    queueLength--;

    return r;
}

總體來看,ReferenceQueue的作用就是JAVA GC與Reference引用對象之間的中間層,我們可以在外部通過ReferenceQueue及時地根據所監聽的對象的可達性狀態變化而採取處理操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章