ReferenceQueue
引用隊列,在檢測到適當的可到達性更改后,垃圾回收器將已注冊的引用對象添加到該隊列中
實現了一個隊列的入隊(enqueue)和出隊(poll還有remove)操作,內部元素就是泛型的Reference,并且Queue的實現,是由Reference自身的鏈表結構( 單向循環鏈表 )所實現的。
ReferenceQueue名義上是一個隊列,但實際內部并非有實際的存儲結構,它的存儲是依賴于內部節點之間的關系來表達。可以理解為queue是一個類似于鏈表的結構,這里的節點其實就是reference本身。可以理解為queue為一個鏈表的容器,其自己僅存儲當前的head節點,而后面的節點由每個reference節點自己通過next來保持即可。
- 屬性
head:始終保存當前隊列中最新要被處理的節點,可以認為queue為一個后進先出的隊列。當新的節點進入時,采取以下的邏輯:
r.next = (head == null) ? r : head;
head = r;
然后,在獲取的時候,采取相應的邏輯:
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 in Reference
r.queue = NULL;
r.next = r;
- 方法
enqueue():待處理引用入隊
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (lock) {
// Check that since getting the lock this reference hasn't already been
// enqueued (and even then removed)
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
r.queue = ENQUEUED;
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll(); // ①
return true;
}
}
① lock.notifyAll(); ??通知外部程序之前阻塞在當前隊列之上的情況。( 即之前一直沒有拿到待處理的對象,如ReferenceQueue的remove()方法 )
Reference
java.lang.ref.Reference 為 軟(soft)引用、弱(weak)引用、虛(phantom)引用的父類。
因為Reference對象和垃圾回收密切配合實現,該類可能不能被直接子類化。
可以理解為Reference的直接子類都是由jvm定制化處理的,因此在代碼中直接繼承于Reference類型沒有任何作用。但可以繼承jvm定制的Reference的子類。
例如:Cleaner 繼承了 PhantomReference
public class Cleaner extends PhantomReference<Object>
構造函數
其內部提供2個構造函數,一個帶queue,一個不帶queue。其中queue的意義在于,我們可以在外部對這個queue進行監控。即如果有對象即將被回收,那么相應的reference對象就會被放到這個queue里。我們拿到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;
}
如果我們在創建一個引用對象時,指定了ReferenceQueue,那么當引用對象指向的對象達到合適的狀態(根據引用類型不同而不同)時,GC 會把引用對象本身添加到這個隊列中,方便我們處理它,因為“引用對象指向的對象 GC 會自動清理,但是引用對象本身也是對象(是對象就占用一定資源),所以需要我們自己清理。”
Reference鏈表結構內部主要的成員有
① pending 和 discovered
/* 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 */
可以理解為jvm在gc時會將要處理的對象放到這個靜態字段上面。同時,另一個字段discovered:表示要處理的對象的下一個對象。即可以理解要處理的對象也是一個鏈表,通過discovered進行排隊,這邊只需要不停地拿到pending,然后再通過discovered不斷地拿到下一個對象賦值給pending即可,直到取到了最有一個。因為這個pending對象,兩個線程都可能訪問,因此需要加鎖處理。
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;
② referent
private T referent; /* Treated specially by GC */
??referent字段由GC特別處理
referent:表示其引用的對象,即我們在構造的時候需要被包裝在其中的對象。對象即將被回收的定義:此對象除了被reference引用之外沒有其它引用了( 并非確實沒有被引用,而是gcRoot可達性不可達,以避免循環引用的問題 )。如果一旦被回收,則會直接置為null,而外部程序可通過引用對象本身( 而不是referent,這里是reference#get() )了解到回收行為的產生( PhntomReference除外 )。
③ next
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
@SuppressWarnings("rawtypes")
Reference next;
next:即描述當前引用節點所存儲的下一個即將被處理的節點。但next僅在放到queue中才會有意義( 因為,只有在enqueue的時候,會將next設置為下一個要處理的Reference對象 )。為了描述相應的狀態值,在放到隊列當中后,其queue就不會再引用這個隊列了。而是引用一個特殊的ENQUEUED。因為已經放到隊列當中,并且不會再次放到隊列當中。
④ discovered
/* 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 */
??被VM使用
discovered:當處于active狀態時:discoverd reference的下一個元素是由GC操縱的( 如果是最后一個了則為this );當處于pending狀態:discovered為pending集合中的下一個元素( 如果是最后一個了則為null );其他狀態:discovered為null
⑤ lock
static private class Lock { }
private static Lock lock = new Lock();
lock:在垃圾收集中用于同步的對象。收集器必須獲取該鎖在每次收集周期開始時。因此這是至關重要的:任何持有該鎖的代碼應該盡快完成,不分配新對象,并且避免調用用戶代碼。
⑥ pending
/* 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;
pending:等待被入隊的引用列表。收集器會添加引用到這個列表,直到Reference-handler線程移除了它們。這個列表被上面的lock對象保護。這個列表使用discovered字段來連接它自己的元素( 即pending的下一個元素就是discovered對象 )。
⑦ queue
volatile ReferenceQueue<? super T> queue;
queue:是對象即將被回收時所要通知的隊列。當對象即被回收時,整個reference對象( 而不是被回收的對象 )會被放到queue里面,然后外部程序即可通過監控這個queue拿到相應的數據了。
這里的queue( 即,ReferenceQueue對象 )名義上是一個隊列,但實際內部并非有實際的存儲結構,它的存儲是依賴于內部節點之間的關系來表達。可以理解為queue是一個類似于鏈表的結構,這里的節點其實就是reference本身。可以理解為queue為一個鏈表的容器,其自己僅存儲當前的head節點,而后面的節點由每個reference節點自己通過next來保持即可。
- Reference 實例( 即Reference中的真是引用對象referent )的4中可能的內部狀態值
Queue的另一個作用是可以區分不同狀態的Reference。Reference有4種狀態,不同狀態的reference其queue也不同:- Active:新創建的引用對象都是這個狀態,在 GC 檢測到引用對象已經到達合適的reachability時,GC 會根據引用對象是否在創建時制定ReferenceQueue參數進行狀態轉移,如果指定了,那么轉移到Pending,如果沒指定,轉移到Inactive。
- Pending:pending-Reference列表中的引用都是這個狀態,它們等著被內部線程ReferenceHandler處理入隊(會調用ReferenceQueue.enqueue方法)。沒有注冊的實例不會進入這個狀態。
- Enqueued:相應的對象已經為待回收,并且相應的引用對象已經放到queue當中了。準備由外部線程來詢問queue獲取相應的數據。調用ReferenceQueue.enqueued方法后的Reference處于這個狀態中。當Reference實例從它的ReferenceQueue移除后,它將成為Inactive。沒有注冊的實例不會進入這個狀態。
- Inactive:即此對象已經由外部從queue中獲取到,并且已經處理掉了。即意味著此引用對象可以被回收,并且對內部封裝的對象也可以被回收掉了( 實際的回收運行取決于clear動作是否被調用 )。可以理解為進入到此狀態的肯定是應該被回收掉的。一旦一個Reference實例變為了Inactive,它的狀態將不會再改變。
jvm并不需要定義狀態值來判斷相應引用的狀態處于哪個狀態,只需要通過計算next和queue即可進行判斷。
- Active:queue為創建一個Reference對象時傳入的ReferenceQueue對象;如果ReferenceQueue對象為空或者沒有傳入ReferenceQueue對象,則為ReferenceQueue.NULL;next==null;
- Pending:queue為初始化時傳入ReferenceQueue對象;next==this(由jvm設置);
- Enqueue:當queue!=null && queue != ENQUEUED 時;設置queue為ENQUEUED;next為下一個要處理的reference對象,或者若為最后一個了next==this;
- Inactive:queue = ReferenceQueue.NULL; next = this.
??外部從queue中獲取Reference
- WeakReference對象進入到queue之后,相應的referent為null。
- SoftReference對象,如果對象在內存足夠時,不會進入到queue,自然相應的referent不會為null。如果需要被處理( 內存不夠或其它策略 ),則置相應的referent為null,然后進入到queue。通過debug發現,SoftReference是pending狀態時,referent就已經是null了,說明此事referent已經被GC回收了。
- FinalReference對象,因為需要調用其finalize對象,因此其reference即使入queue,其referent也不會為null,即不會clear掉。
- PhantomReference對象,因為本身get實現為返回null。因此clear的作用不是很大。因為不管enqueue還是沒有,都不會清除掉。
Q:??如果PhantomReference對象不管enqueue還是沒有,都不會清除掉reference對象,那么怎么辦?這個reference對象不就一直存在這了??而且JVM是會直接通過字段操作清除相應引用的,那么是不是JVM已經釋放了系統底層資源,但java代碼中該引用還未置null??
A:不會的,雖然PhantomReference有時候不會調用clear,如Cleaner對象 。但Cleaner的clean()方法只調用了remove(this),這樣當clean()執行完后,Cleaner就是一個無引用指向的對象了,也就是可被GC回收的對象。
active ——> pending :Reference#tryHandlePending
pending ——> enqueue :ReferenceQueue#enqueue
enqueue ——> inactive :Reference#clear
重要方法
① clear()
/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
*
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}
調用此方法不會導致此對象入隊。此方法僅由Java代碼調用;當垃圾收集器清除引用時,它直接執行,而不調用此方法。
clear的語義就是將referent置null。
清除引用對象所引用的原對象,這樣通過get()方法就不能再訪問到原對象了( PhantomReference除外 )。從相應的設計思路來說,既然都進入到queue對象里面,就表示相應的對象需要被回收了,因為沒有再訪問原對象的必要。此方法不會由JVM調用,而JVM是直接通過字段操作清除相應的引用,其具體實現與當前方法相一致。
② ReferenceHandler線程
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
其優先級最高,可以理解為需要不斷地處理引用對象。
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 {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
③ tryHandlePending()
/**
* Try handle pending {@link Reference} if there is one.<p>
* Return {@code true} as a hint that there might be another
* {@link Reference} pending or {@code false} when there are no more pending
* {@link Reference}s at the moment and the program can do some other
* useful work instead of looping.
*
* @param waitForNotify if {@code true} and there was no pending
* {@link Reference}, wait until notified from VM
* or interrupted; if {@code false}, return immediately
* when there is no pending {@link Reference}.
* @return {@code true} if there was a {@link Reference} pending and it
* was processed, or we waited for notification and either got it
* or thread was interrupted before being notified;
* {@code false} otherwise.
*/
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
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;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
這個線程在Reference類的static構造塊中啟動,并且被設置為高優先級和daemon狀態。此線程要做的事情,是不斷的檢查pending 是否為null,如果pending不為null,則將pending進行enqueue,否則線程進入wait狀態。
由此可見,pending是由jvm來賦值的,當Reference內部的referent對象的可達狀態改變時,jvm會將Reference對象放入pending鏈表。并且這里enqueue的隊列是我們在初始化( 構造函數 )Reference對象時傳進來的queue,如果傳入了null( 實際使用的是ReferenceQueue.NULL ),則ReferenceHandler則不進行enqueue操作,所以只有非RefernceQueue.NULL的queue才會將Reference進行enqueue。
ReferenceQueue是作為 JVM GC與上層Reference對象管理之間的一個消息傳遞方式,它使得我們可以對所監聽的對象引用可達發生變化時做一些處理
參考
http://www.importnew.com/21633.html
http://hongjiang.info/java-referencequeue/
http://www.cnblogs.com/jabnih/p/6580665.html
http://www.importnew.com/20468.html
http://liujiacai.net/blog/2015/09/27/java-weakhashmap/