1. 程式人生 > >JDK 源碼閱讀 Reference

JDK 源碼閱讀 Reference

prior 垃圾回收器 有關 hand 什麽 我們 最終 關聯 算法

Java最初只有普通的強引用,只有對象存在引用,則對象就不會被回收,即使內存不足,也是如此,JVM會爆出OOME,也不會去回收存在引用的對象。

如果只提供強引用,我們就很難寫出“這個對象不是很重要,如果內存不足GC回收掉也是可以的”這種語義的代碼。Java在1.2版本中完善了引用體系,提供了4中引用類型:強引用,軟引用,弱引用,虛引用。使用這些引用類型,我們不但可以控制垃圾回收器對對象的回收策略,同時還能在對象被回收後得到通知,進行相應的後續操作。

引用與可達性分類

Java目前有4中引用類型:

強引用(Strong Reference):普通的的引用類型,new一個對象默認得到的引用就是強引用,只要對象存在強引用,就不會被GC。

軟引用(Soft Reference):相對較弱的引用,垃圾回收器會在內存不足時回收弱引用指向的對象。JVM會在拋出OOME前清理所有弱引用指向的對象,如果清理完還是內存不足,才會拋出OOME。所以軟引用一般用於實現內存敏感緩存。
弱引用(Weak Reference):更弱的引用類型,垃圾回收器在GC時會回收此對象,也可以用於實現緩存,比如JDK提供的WeakHashMap。
虛引用(Phantom Reference):一種特殊的引用類型,不能通過虛引用獲取到關聯對象,只是用於獲取對象被回收的通知。
相較於傳統的引用計數算法,Java使用可達性分析來判斷一個對象是否存活。其基本思路是從GC Root開始向下搜索,如果對象與GC Root之間存在引用鏈,則對象是可達的。對象的可達性與引用類型密切相關。Java有5中類型的可達性:

強可達(Strongly Reachable):如果線程能通過強引用訪問到對象,那麽這個對象就是強可達的。
軟可達(Soft Reachable):如果一個對象不是強可達的,但是可以通過軟引用訪問到,那麽這個對象就是軟可達的
弱可達(Weak Reachable):如果一個對象不是強可達或者軟可達的,但是可以通過弱引用訪問到,那麽這個對象就是弱可達的。
虛可達(Phantom Reachable):如果一個對象不是強可達,軟可達或者弱可達,並且這個對象已經finalize過了,並且有虛引用指向該對象,那麽這個對象就是虛可達的。
不可達(Unreachable):如果對象不能通過上述的幾種方式訪問到,則對象是不可達的,可以被回收。

對象的引用類型與可達性聽著有點亂,好像是一回事,我們這裏實例分析一下:

上面這個例子中,A~D,每個對象只存在一個引用,分別是:A-強引用,B-軟引用,C-弱引用,D-虛引用,所以他們的可達性為:A-強可達,B-軟可達,C-弱可達,D-虛可達。因為E沒有存在和GC Root的引用鏈,所以它是不可達。

在看一個復雜的例子:

A依然只有一個強引用,所以A是強可達
B存在兩個引用,強引用和軟引用,但是B可以通過強引用訪問到,所以B是強可達
C只能通過弱引用訪問到,所以是弱可達
D存在弱引用和虛引用,所以是弱可達
E雖然存在F的強引用,但是GC Root無法訪問到它,所以它依然是不可達。
同時可以看出,對象的可達性是會發生變化的,隨著運行時引用對象的引用類型的變化,可達性也會發生變化,可以參考下圖:

Reference總體結構

Reference類是所有引用類型的基類,Java提供了具體引用類型的具體實現:

SoftReference:軟引用,堆內存不足時,垃圾回收器會回收對應引用
WeakReference:弱引用,每次垃圾回收都會回收其引用
PhantomReference:虛引用,對引用無影響,只用於獲取對象被回收的通知
FinalReference:Java用於實現finalization的一個內部類
因為默認的引用就是強引用,所以沒有強引用的Reference實現類。

Reference的核心

Java的多種引用類型實現,不是通過擴展語法實現的,而是利用類實現的,Reference類表示一個引用,其核心代碼就是一個成員變量reference:

1
2
3
4
5
6
7
8
9
10
public abstract class Reference<T> {
private T referent; // 會被GC特殊對待

// 獲取Reference管理的對象
public T get() {
    return this.referent;
}

// ...

}
如果JVM沒有對這個變量做特殊處理,它依然只是一個普通的強引用,之所以會出現不同的引用類型,是因為JVM垃圾回收器硬編碼識別SoftReference,WeakReference,PhantomReference等這些具體的類,對其reference變量進行特殊對象,才有了不同的引用類型的效果。

上文提到了Reference及其子類有兩大功能:

實現特定的引用類型
用戶可以對象被回收後得到通知
第一個功能已經解釋過了,第二個功能是如何做到的呢?

一種思路是在新建一個Reference實例是,添加一個回調,當java.lang.ref.Reference#referent被回收時,JVM調用該回調,這種思路比較符合一般的通知模型,但是對於引用與垃圾回收這種底層場景來說,會導致實現復雜,性能不高的問題,比如需要考慮在什麽線程中執行這個回調,回調執行阻塞怎麽辦等等。

所以Reference使用了一種更加原始的方式來做通知,就是把引用對象被回收的Reference添加到一個隊列中,用戶後續自己去從隊列中獲取並使用。

理解了設計後對應到代碼上就好理解了,Reference有一個queue成員變量,用於存儲引用對象被回收的Reference實例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Reference<T> {
// 會被GC特殊對待
private T referent;
// reference被回收後,當前Reference實例會被添加到這個隊列中
volatile ReferenceQueue<? super T> queue;

// 只傳入reference的構造函數,意味著用戶只需要特殊的引用類型,不關心對象何時被GC
Reference(T referent) {
    this(referent, null);
}

// 傳入referent和ReferenceQueue的構造函數,reference被回收後,會添加到queue中
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

// ...

}
Reference的狀態

Reference對象是有狀態的。一共有4中狀態:

Active:新創建的實例的狀態,由垃圾回收器進行處理,如果實例的可達性處於合適的狀態,垃圾回收器會切換實例的狀態為Pending或者Inactive。如果Reference註冊了ReferenceQueue,則會切換為Pending,並且Reference會加入pending-Reference鏈表中,如果沒有註冊ReferenceQueue,會切換為Inactive。
Pending:在pending-Reference鏈表中的Reference的狀態,這些Reference等待被加入ReferenceQueue中。
Enqueued:在ReferenceQueue隊列中的Reference的狀態,如果Reference從隊列中移除,會進入Inactive狀態
Inactive:Reference的最終狀態
Reference對象圖如下:

除了上文提到的ReferenceQueue,這裏出現了一個新的數據結構:pending-Reference。這個鏈表是用來幹什麽的呢?

上文提到了,reference引用的對象被回收後,該Reference實例會被添加到ReferenceQueue中,但是這個不是垃圾回收器來做的,這個操作還是有一定邏輯的,如果垃圾回收器還需要執行這個操作,會降低其效率。從另外一方面想,Reference實例會被添加到ReferenceQueue中的實效性要求不高,所以也沒必要在回收時立馬加入ReferenceQueue。

所以垃圾回收器做的是一個更輕量級的操作:把Reference添加到pending-Reference鏈表中。Reference對象中有一個pending成員變量,是靜態變量,它就是這個pending-Reference鏈表的頭結點。要組成鏈表,還需要一個指針,指向下一個節點,這個對應的是java.lang.ref.Reference#discovered這個成員變量。

可以看一下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Reference<T> {
// 會被GC特殊對待
private T referent;
// reference被回收後,當前Reference實例會被添加到這個隊列中
volatile ReferenceQueue<? super T> queue;

// 全局唯一的pending-Reference列表
private static Reference<Object> pending = null;

// Reference為Active:由垃圾回收器管理的已發現的引用列表(這個不在本文討論訪問內)
// Reference為Pending:在pending列表中的下一個元素,如果沒有為null
// 其他狀態:NULL
transient private Reference<T> discovered;  /* used by VM */
// ...

}

ReferenceHandler線程

通過上文的討論,我們知道一個Reference實例化後狀態為Active,其引用的對象被回收後,垃圾回收器將其加入到pending-Reference鏈表,等待加入ReferenceQueue。這個過程是如何實現的呢?

這個過程不能對垃圾回收器產生影響,所以不能在垃圾回收線程中執行,也就需要一個獨立的線程來負責。這個線程就是ReferenceHandler,它定義在Reference類中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 用於控制垃圾回收器操作與Pending狀態的Reference入隊操作不沖突執行的全局鎖
// 垃圾回收器開始一輪垃圾回收前要獲取此鎖
// 所以所有占用這個鎖的代碼必須盡快完成,不能生成新對象,也不能調用用戶代碼
static private class Lock { };
private static Lock lock = new Lock();

private static class ReferenceHandler extends Thread {

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

public void run() {
    // 這個線程一直執行
    for (;;) {
        Reference<Object> r;
        // 獲取鎖,避免與垃圾回收器同時操作
        synchronized (lock) {
            // 判斷pending-Reference鏈表是否有數據
            if (pending != null) {
                // 如果有Pending Reference,從列表中取出
                r = pending;
                pending = r.discovered;
                r.discovered = null;
            } else {
                // 如果沒有Pending Reference,調用wait等待
                // 
                // wait等待鎖,是可能拋出OOME的,
                // 因為可能發生InterruptedException異常,然後就需要實例化這個異常對象,
                // 如果此時內存不足,就可能拋出OOME,所以這裏需要捕獲OutOfMemoryError,
                // 避免因為OOME而導致ReferenceHandler進程靜默退出
                try {
                    try {
                        lock.wait();
                    } catch (OutOfMemoryError x) { }
                } catch (InterruptedException x) { }
                continue;
            }
        }

        // 如果Reference是Cleaner,調用其clean方法
        // 這與Cleaner機制有關系,不在此文的討論訪問
        if (r instanceof Cleaner) {
            ((Cleaner)r).clean();
            continue;
        }

        // 把Reference添加到關聯的ReferenceQueue中
        // 如果Reference構造時沒有關聯ReferenceQueue,會關聯ReferenceQueue.NULL,這裏就不會進行入隊操作了
        ReferenceQueue<Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
}

}
ReferenceHandler線程是在Reference的static塊中啟動的:

1
2
3
4
5
6
7
8
9
10
11
12
13
static {
// 獲取system ThreadGroup
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");

// ReferenceHandler線程有最高優先級
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();

}
綜上,ReferenceHandler是一個最高優先級的線程,其邏輯是從Pending-Reference鏈表中取出Reference,添加到其關聯的Reference-Queue中。

ReferenceQueue

Reference-Queue也是一個鏈表:

1
2
3
4
public class ReferenceQueue<T> {
private volatile Reference<? extends T> head = null;
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ReferenceQueue中的這個鎖用於保護鏈表隊列在多線程環境下的正確性
static private class Lock { };
private Lock lock = new Lock();

boolean enqueue(Reference<? extends T> r) { / Called only by Reference class /
synchronized (lock) {
// 判斷Reference是否需要入隊
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;

    // Reference入隊後,其queue變量設置為ENQUEUED
    r.queue = ENQUEUED;
    // Reference的next變量指向ReferenceQueue中下一個元素
    r.next = (head == null) ? r : head;
    head = r;
    queueLength++;
    if (r instanceof FinalReference) {
        sun.misc.VM.addFinalRefCount(1);
    }
    lock.notifyAll();
    return true;
}

}

通過上面的代碼,可以知道java.lang.ref.Reference#next的用途了:

1
2
3
4
5
6
7
8
9
10
public abstract class Reference<T> {
/* When active: NULL

  • pending: this
  • Enqueued: 指向ReferenceQueue中的下一個元素,如果沒有,指向this
  • Inactive: this
    */
    Reference next;

    // ...
    }
    總結

一個使用Reference+ReferenceQueue的完整流程如下:

JDK 源碼閱讀 Reference