一提到Reference 百分之九十九的java程式設計師都懵逼了
原來的標題是:"一提到Reference 99.99%的java程式設計師都懵逼了",為啥改成漢字了呢?吐槽一下,因為CSDN出bug了,如果你用了%做標題,你的文章就別想用它的編輯器修改了,它的js指令碼寫的不夠健壯,報錯了;
java.lang.ref.Reference
java程式設計師都知道如果一個物件沒有任何引用了,那麼這個物件在gc的時候就被回收了,大部分java程式設計師是基於我們常見的強引用去理解引用的,強引用這種方式雖然簡單直接,但是對於一些特殊的java物件,如快取資料在記憶體緊張時自動釋放掉空間防止oom、直接記憶體物件回收之前需自動釋放掉其佔用的堆外記憶體,當socket物件被回收之前關閉連線,當檔案流物件被回收之前自動關閉開啟的檔案等操作,強引用就無能為力了,為了實現這些特殊需求,java還引入了除了強引以外的引用型別來輔助物件回收操作 或 監控物件的生存週期。
先來看看引用型別的大家族:
在java.lang.ref包下除了ReferenceQueue類,Finalizer、FinalReference、PhantomReference、SoftReference、WeakReference都直接或間接繼承了Reference類,想想為什麼沒有強引用StrongReference?
理解Reference的關鍵點在於:
1. 兩個佇列pending與ReferenceQueue的作用;
2. Reference物件4種狀態的轉換;
3. ReferenceHander執行緒的處理過程,它是如何將Pending與ReferenceQueue關聯起來的;
4. Pending佇列資料來源;
理解了以上4個問題,才算真不再對Reference懵逼了,下面就帶著這四個問題去看下面的內容。
Reference構造方法
我們先來看看這些引用型別的父類都給我們提供了什麼?
其內部提供2個建構函式,除了傳入referent物件外,區別就是要不要傳入一個ReferenceQueue佇列
Reference(Treferent) {
this(referent,null);
}
Reference(T referent,ReferenceQueue<?super T>queue){
this.referent =referent;
this.queue = (queue==null)? ReferenceQueue.NULL:queue;
}
ReferenceQueue佇列的作用就是Reference引用的物件被回收時,Reference物件能否進入到pending佇列,最終由ReferenceHander執行緒處理後,Reference就被放到了這個佇列裡面(Cleaner物件除外,後面有一篇專門講Cleaner物件原始碼),然後我們就可以在這個ReferenceQueue裡拿到reference,執行我們自己的操作(至於什麼操作就看你想怎麼用了),所以這個佇列起到一個物件被回收時通知的作用;
如果不帶ReferenceQueue的話,要想知道Reference持有的物件是否被回收,就只有不斷地輪訓reference物件,通過判斷裡面的get是否為null(phantomReference物件不能這樣做,其get始終返回null,因此它只有帶queue的建構函式).這兩種方法均有相應的使用場景,取決於實際的應用.如weakHashMap中就選擇去查詢queue的資料,來判定是否有物件將被回收.而ThreadLocalMap,則採用判斷get()是否為null來作處理;
對於帶ReferenceQueue引數的構造方法,如果傳入的佇列為null,那麼就會給成員變數queue賦值為ReferenceQueue.NULL佇列,這個NULL是ReferenceQueue物件的一個繼承了ReferenceQueue的內部類,它重寫了入隊方法enqueue,這個方法只有一個操作,直接返回 false,也就是這個對列不會存取任何資料,它起到狀態標識的作用;
Reference的重要成員變數
//在構造方法傳入java物件最後就賦值給了referent,也就是它引用到的物件
private Treferent; /* Treated specially by GC */
//pending佇列, pending成員變數與後面的discovered物件一起構成了一個pending單向連結串列,注意這個成員變數是一個靜態物件,所以是全域性唯一的,pending為連結串列的頭節點,discovered為連結串列當前Reference節點指向下一個節點的引用,這個佇列是由jvm的垃圾回收器構建的,當物件除了被reference引用之外沒有其它強引用了,jvm的垃圾回收器就會將指向需要回收的物件的Reference都放入到這個佇列裡面(好好理解一下這句話,注意是指向要回收的物件的Reference,要回收的物件就是Reference的成員變數refernt持有的物件,是refernt持有的物件要被回收,而不是Reference物件本身),這個佇列會由ReferenceHander執行緒來處理(ReferenceHander執行緒是jvm的一個內部執行緒,它也是Reference的一個內部類,它的任務就是將pending佇列中要被回收的Reference物件移除出來,如果Reference物件在初始化的時候傳入了ReferenceQueue佇列,那麼就把從pending佇列裡面移除的Reference放到它自己的ReferenceQueue佇列裡(為什麼是它自己的?pending佇列是全域性唯一的佇列,但是Reference的queue卻不是,它是在構造方法裡面指定的,前面說過這裡Cleaner物件是個特例),如果沒有ReferenceQueue佇列,那麼其關聯的物件就不會進入到Pending佇列中,會直接被回收掉,除此之外ReferenceHander執行緒還會做一些其它操作,後面會講到
/* List of References waiting to beenqueued. The collector adds
* References to this list, while theReference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to linkits elements.
*/
privatestatic Reference<Object>pending =null;
//與成員變數pending一起組成pending佇列,指向連結串列當前節點的下一個節點
/* When active: next element in a discovered reference listmaintained by GC (or this if last)
* pending: next element in thepending list (or null if last)
* otherwise: NULL
*/
transientprivate Reference<T>discovered; /* used by VM */
//ReferenceQueue佇列,這就是我們前面提到的起到通知作用的ReferenceQueue,需要注意的是ReferenceQueue並不是一個連結串列資料結構,它只持有這個連結串列的表頭物件header,這個連結串列是由Refence物件裡面的next成員變數構建起來的,next也就是連結串列當前節點的下一個節點(只有next的引用,它是單向連結串列),所以Reference物件本身就是一個連結串列的節點,這個連結串列資料來源前面講pending佇列的時候已經提到了,它是由ReferenceHander執行緒從pending佇列中取的資料構建的(需要注意的是,這個物件並不是一個全域性的,它是在構造方法裡面傳入進來的,所以Reference物件需要進入那個佇列是我們自己指定的,也有特例,如FinalReference,Cleaner就是一個內部全域性唯一的,無法指定,後面有兩篇專門講他們倆的原始碼),一旦Reference物件放入了佇列裡面,那麼queue就會被設定為ReferenceQueue.ENQUEUED,來標識當前Reference已經進入到隊裡裡面了;
volatileReferenceQueue<?super T>queue;
//用來與queue成員變數一同組成ReferenceQueue佇列,見上面queue的說明;
/* When active: NULL
* pending: this
* Enqueued: next reference inqueue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
Reference next;
//lock成員變數是pending佇列的全域性鎖,如果你搜尋這個lock變數會發現它只在ReferenceHander執行緒run方法裡面用到了,不要忘了jvm垃圾回收器執行緒也會操作pending佇列,往pending裡面新增Reference物件,所以需要加鎖;
/* Object used to synchronize with thegarbage collector. The collector
* must acquire this lock at the beginningof each collection cycle. It is
* therefore critical that any code holdingthis lock complete as quickly
* as possible, allocate no new objects,and avoid calling user code.
*/
staticprivateclass Lock { };
privatestatic Locklock =new Lock();
這裡一定要理解pending佇列,什麼樣的Reference物件會進入這個佇列。
進入這個佇列的Reference物件需要滿足兩個條件:
1. Reference所引用的物件已經不存在其它強引用;
2. Reference物件在建立的時候,指定了ReferenceQueue;
Reference狀態及其轉換
如果去檢視Reference原始碼,會發現這個類的開頭有一段很長的註釋,說明了Reference物件的四種狀態:
/* A Reference instance is in one of four possible internal states:
*
* Active: Subject to special treatment by the garbage collector. Some
* time after the collector detects that the reachability of the
* referent has changed to the appropriate state, it changes the
* instance's state to either Pending or Inactive, depending upon
* whether or not the instance was registered with a queue when it was
* created. In the former case it also adds the instance to the
* pending-Reference list. Newly-created instances are Active.
*
* Pending: An element of the pending-Reference list, waiting to be
* enqueued by the Reference-handler thread. Unregistered instances
* are never in this state.
*
* Enqueued: An element of the queue with which the instance was
* registered when it was created. When an instance is removed from
* its ReferenceQueue, it is made Inactive. Unregistered instances are
* never in this state.
*
* Inactive: Nothing more to do. Once an instance becomes Inactive its
* state will never change again.
*
* The state is encoded in the queue and next fields as follows:
*
* Active: queue = ReferenceQueue with which instance is registered, or
* ReferenceQueue.NULL if it was not registered with a queue; next =
* null.
*
* Pending: queue = ReferenceQueue with which instance is registered;
* next = this
*
* Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
* in queue, or this if at end of list.
*
* Inactive: queue = ReferenceQueue.NULL; next = this.
*
* With this scheme the collector need only examine the next field in order
* to determine whether a Reference instance requires special treatment: If
* the next field is null then the instance is active; if it is non-null,
* then the collector should treat the instance normally.
*
* To ensure that a concurrent collector can discover active Reference
* objects without interfering with application threads that may apply
* the enqueue() method to those objects, collectors should link
* discovered objects through the discovered field. The discovered
* field is also used for linking Reference objects in the pending list.
*/
如果你沒有耐心看完這段註釋,請直接往後看:
1. Active:活動狀態,物件存在強引用狀態,還沒有被回收;
2. Pending:垃圾回收器將沒有強引用的Reference物件放入到pending佇列中,等待ReferenceHander執行緒處理(前提是這個Reference物件建立的時候傳入了ReferenceQueue,否則的話物件會直接進入Inactive狀態);
3. Enqueued:ReferenceHander執行緒將pending佇列中的物件取出來放到ReferenceQueue佇列裡;
4. Inactive:處於此狀態的Reference物件可以被回收,並且其內部封裝的物件也可以被回收掉了,有兩個路徑可以進入此狀態,
路徑一:在建立時沒有傳入ReferenceQueue的Reference物件,被Reference封裝的物件在沒有強引用時,指向它的Reference物件會直接進入此狀態;
路徑二、此Reference物件經過前面三個狀態後,已經由外部從ReferenceQueue中獲取到,並且已經處理掉了。
Reference物件的狀態只需要通過成員變數next和queue來判斷:
1. Active: next=null
2. Pending: next = this ,queue = ReferenceQueue
3. Enqueued: queue =ReferenceQueue.ENQUEUED
4. Inactive: next = this ,queue = ReferenceQueue.NULL;
以下是Reference物件狀態轉換圖,用word畫的,將就著看吧:
特例還是要拿出來單獨說,上面的圖不適用描述Cleaner物件,Cleaner物件是沒有Enqueue狀態的,它經過HandReference處理時執行其clean方法清理,然後就直接進入了inactive狀態了;
下面簡單看一下他們的原始碼實現,如果你理解了上面所有內容,下面的原始碼理解起來非常簡單,我只做簡單的註釋說明:
ReferenceHandler執行緒原始碼解析
ReferenceHandler執行緒是一個擁有最高優先順序的守護執行緒,它是Reference類的一個內部類,在Reference類載入執行cinit的時候被初始化並啟動;它的任務就是當pending佇列不為空的時候,迴圈將pending佇列裡面的頭部的Reference移除出來,如果這個物件是個Cleaner例項,那麼就直接執行它的clean方法來執行清理工作;否則放入到它自己的ReferenceQueue裡面;
所以這個執行緒是pending佇列與ReferenceQueue的橋樑;
/* High-priority thread to enqueue pending References*/
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
//從pending中移除的Reference物件
Reference<Object> r;
//此處需要加全域性鎖,因為除了當前執行緒,gc執行緒也會操作pending佇列
synchronized (lock) {
//如果pending佇列不為空,則將第一個Reference物件取出
if (pending != null) {
//快取pending佇列頭節點
r = pending;
//將頭節點指向discovered,discovered為pending佇列中當前節點的下一個節點,這樣就把第一個頭結點出隊了
pending = r.discovered;
//將當前節點的discovered設定為null;當前節點出隊,不需要組成連結串列了;
r.discovered = null;
} else {//如果pending佇列為空,則等待
try {
try {
lock.wait();
} catch(OutOfMemoryError x) { }
} catch(InterruptedException x) { }
continue;
}
}
// 如果從pending佇列出隊的r是一個Cleaner物件,那麼直接執行其clean()方法執行清理操作;
if (r instanceof Cleaner) {
((Cleaner)r).clean();
//注意這裡,這裡已經不往下執行了,所以Cleaner物件是不會進入到佇列裡面的,給它設定ReferenceQueue的作用是為了讓它能進入Pending佇列後被ReferenceHander執行緒處理;
continue;
}
//將物件放入到它自己的ReferenceQueue佇列裡
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
//以下是ReferenceHander執行緒初始化並啟動的操作
static {
ThreadGroup tg =Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
//執行緒名稱為Reference Handler
Thread handler = newReferenceHandler(tg, "Reference Handler");
/* If there were a special system-onlypriority greater than
* MAX_PRIORITY, it would be used here
*/
//執行緒有最高優先順序
handler.setPriority(Thread.MAX_PRIORITY);
//設定執行緒為守護執行緒;
handler.setDaemon(true);
handler.start();
}
ReferenceQueue原始碼解析
ReferenceQueue佇列是一個單向連結串列,ReferenceQueue裡面只有一個header成員變數持有佇列的隊頭,Reference物件是從隊頭做出隊入隊操作,所以它是一個後進先出的佇列
public class ReferenceQueue<T> {
public ReferenceQueue() { }
//內部類,它是用來做狀態識別的,重寫了enqueue入隊方法,永遠返回false,所以它不會儲存任何資料,見後面的NULL和ENQUEUED兩個標識成員變數
private static class Null<S> extendsReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false;
}
}
//當Reference物件建立時沒有指定queue或Reference物件已經處於inactive狀態
staticReferenceQueue<Object> NULL = new Null<>();
//當Reference已經被ReferenceHander執行緒從pending佇列移到queue裡面時
static ReferenceQueue<Object> ENQUEUED = new Null<>();
staticprivate class Lock { };
//出隊入隊時對佇列加鎖
privateLock lock = new Lock();
//佇列頭
privatevolatile Reference<? extends T> head = null;
//佇列長度
private long queueLength = 0;
//入隊操作,ReferenceHander呼叫此方法將Reference放入到佇列裡
boolean enqueue(Reference<? extends T> r) { /* Called only byReference class */
//加鎖操作佇列
synchronized(lock) {
//如果Reference建立時沒有指定佇列或Reference物件已經在佇列裡面了,則直接返回
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
//只有r的佇列是當前佇列才允許入隊
assert queue == this;
//將r的queue設定為ENQUEUED狀態,標識Reference已經入隊
r.queue = ENQUEUED;
//從佇列頭部入隊
r.next = (head == null) ? r : head;
head = r;
//佇列裡面物件數量+1
queueLength++;
//如果r是一個FinalReference例項,那麼將FinalReference數量也+1
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
/**Vm.addFinalRefCount(int n)方法的原始碼
publicstatic void addFinalRefCount(int n) {
// The caller must hold lock to synchronize the update.
finalRefCount += n;
if (finalRefCount > peakFinalRefCount) {
peakFinalRefCount = finalRefCount;
}
}
可以通過sun.misc.VM.getFinalRefCount()和sun.misc.VM.getPeakFinalRefCount()來獲取FinalReference物件的當前數量和峰值數量
***/
}
//喚醒出隊操作的等待執行緒
lock.notifyAll();
return true;
}
}
//Reference物件出隊操作,將頭部第一個物件移出佇列,並將佇列長度-1
@SuppressWarnings("unchecked")
private Reference<? extends T> reallyPoll() { /* Must hold lock */
Reference<? extends T> r = head;
if (r != null) {
head = (r.next == r) ?
null :
r.next; // Unchecked due to the next field having a raw type inReference
r.queue = NULL;
r.next = r;
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
return r;
}
return null;
}
//將佇列頭部第一個物件從佇列中移除出來,如果佇列為空則直接返回null(此方法不會被阻塞)
public Reference<? extends T> poll() {
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
//將頭部第一個物件移出佇列並返回,如果佇列為空,則等待timeout時間後,返回null,這個方法會阻塞執行緒
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;
for (;;) {
lock.wait(timeout);
r = reallyPoll();
if (r != null) return r;
if (timeout != 0) return null;
}
}
}
public Reference<? extends T> remove() throws InterruptedException{
return remove(0);
}
}
相關文章
- Java程式設計師都需要懂的「反射」Java程式設計師反射
- Java 程式設計師都該懂的 HashMapJava程式設計師HashMap
- 新手程式設計師?教你解決辦法!基礎都掌握了,動手敲程式碼就一臉懵逼程式設計師
- 程式設計狂人-第九十九期程式設計
- 年薪50萬的Java程式設計師,都趟過哪些坑?Java程式設計師
- 程式設計師什麼都會程式設計師
- 程式設計師必看的書之Java程式設計師程式設計師Java
- 程式設計師都應該知道的福利程式設計師
- 接手了個專案,被if..else搞懵逼了
- 國外程式設計師推薦:每個程式設計師都應讀的書程式設計師
- 每個程式設計師都必須遵守的程式設計原則程式設計師
- 程式設計師都幹些什麼?程式設計師
- 程式設計師都該懂點 HTTP程式設計師HTTP
- 國外程式設計師推薦:每個程式設計師都應該讀的非程式設計書程式設計師
- Java程式設計師的錯Java程式設計師
- 好程式設計師:Java程式設計師面試秘籍程式設計師Java面試
- 程式設計師都這樣哄老婆的嗎?程式設計師
- 每個程式設計師都需要了解的一個SQL技巧程式設計師SQL
- [轉貼]一個JAVA 程式設計師的告白Java程式設計師
- BATJTMD,大廠招聘,都招什麼樣Java程式設計師?BATJava程式設計師
- 月薪2w以上的java程式設計師面試都會問的問題Java程式設計師面試
- 程式設計師都遇到過哪些誤解?程式設計師
- 好程式設計師Java培訓分享Java程式設計師常用的工具類庫程式設計師Java
- 優秀的程式設計師都熱愛寫作程式設計師
- 每個程式設計師都應該讀的書程式設計師
- 程式設計師都應該懂一點開源協議程式設計師協議
- ●招聘● Java程式設計師Java程式設計師
- JAVA程式設計師之路Java程式設計師
- 每一個程式設計師都應當瞭解的11句話程式設計師
- 程式設計師都應該挖一口屬於自己的井程式設計師
- Rework:每個程式設計師都應該讀的一本書程式設計師
- 百分之 95% 的程式設計師不知道 Trending 是什麼。程式設計師
- Java程式設計師的墮落Java程式設計師
- 優秀Java程式設計師的程式設計風格Java程式設計師
- 一個 Java 程式設計師眼中的 Go 語言Java程式設計師Go
- 同事問我MySQL怎麼遞迴查詢,我懵逼了MySql遞迴
- 好程式設計師Java培訓Java程式設計師必學技術程式設計師Java
- 好程式設計師Java培訓分享Java程式設計師技能提升指南程式設計師Java