1 狀態機
狀態轉移圖
@startuml
[*] -> active: 新建
active -down-> inactive: 可達性改變且無注冊隊列
active -right-> pending: 可達性改變且有注冊隊列
pending -down-> enqueued: 成功加入隊列
enqueued -left-> inactive: 從隊列移除
inactive -left-> [*]
@enduml
說明:
-
active
。此狀態需要收集器進行特殊處理。當收集器檢測到referent的可達性發生改變,它會將實例的狀態改為為pending
或inactive
,這取決于實例在創建時是否已注冊到某個隊列。有注冊隊列情況,實例會添加到pending-Reference列表中,等待入隊。新建引用實例是活動狀態。 -
pending
。表示pending-Reference列表的元素,等待被Reference-handler處理并入隊。未注冊引用實例不會有此狀態。。 -
enqueued
。表示在注冊隊列中的元素。當實例從ReferenceQueue
中移除時,它將變為inactive
。未注冊引用實例不會有此狀態。盡管Reference
提供了入隊方法,但是收集器執行的入隊操作是直接執行的,而不是調用Reference
的入隊方法。 -
inactive
。終止態,不會在改變。
狀態編碼
引用的狀態是通過不同字段值共同體現的。
狀態 | queue |
next |
discovered |
---|---|---|---|
active |
有注冊隊列時ReferenceQueue 實例或者 未注冊隊列時 ReferenceQueue.NULL
|
NULL |
下一個discovered 列表元素或者如果是隊尾元素則是 this
|
pending |
引用所注冊的ReferenceQueue 實例 |
this |
下一個pending 列表元素或者如果是隊尾元素則是 NULL
|
enqueued |
ReferenceQueue.ENQUEUED |
下一個入隊的實例 或者如果是隊尾元素則是 this
|
NULL |
inactive |
ReferenceQueue.NULL |
this |
NULL |
如何判斷引用狀態是否是active
?
基于以上編碼關系,垃圾收集器只需要對next
字段進行判斷,決定是不是需要進行特殊處理。規則如下:如果next
字段為NULL
,那么實例是active
的;否則,收集器將會按照普通情況處理。
discovered
字段的用途?
當引用狀態為active
時,為了保證并發收集器能夠發現下一個active
引用,同時不影響應用線程對這些active
引用執行enqueue
操作,收集器使用了discovered
字段記錄了下一個active
引用。
當引用狀態為pending
時,discovered
字段還記錄了pending列表中的下一個引用。
引用隊列的用途?
如果程序需要感知對象可達性的變化時,那么要在創建引用對象時傳入注冊隊列。當垃圾收集器發現referent
可達性發生變化時,會將referent
的引用加入到注冊隊列中。此時,引用處于enqueued
狀態。程序可以通過阻塞輪詢的方式,從隊列中移除引用。[2]
已注冊引用和引用注冊隊列的關系是單向的。也就是說,引用注冊隊列不會跟蹤已注冊引用的狀態。如果已注冊引用的狀態變為不可達,那么它永遠不會進入引用注冊隊列。所以,程序需要保證referent
對象的可達性。
如果保證referent
的可達性呢?
一種方式,使用單獨的線程輪詢,并從隊列中刪除引用對象,然后對其進行處理。
另一種方式,在執行操作引用時進行檢查(lazy)。
例如,使用弱引用實現弱鍵的哈希表,可以在每次訪問時輪詢其引用隊列。這就是WeakHashMap
類的工作原理。由于ReferenceQueue.poll
方法只檢查內部數據結構,因此,將為哈希表訪問方法增加很少的開銷。
引用是何時入隊的?
收集器在將軟引用和弱引用加入注冊隊列(如果有的話)之前,自動清除軟引用和弱引用。因此,軟引用和弱引用不需要注冊到隊列中才能發揮作用,而虛引用則需要注冊隊列。虛引用對象會保持可達,除非虛引用被清除或者虛引用對象本身不可達。
public static void main(String[] args) {
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = // 如果不聲明本地變量,在gc后將會入隊
new PhantomReference<>(new Object(), referenceQueue);
System.gc();
System.out.println("Object in queue: " + referenceQueue.poll());
}
2 tryHandlePending
處理pending
列表中的引用對象。
參考如下代碼,可見處理過程中,體現了這樣一點。discovered
字段記錄這pending
列表中的下一個元素。
Reference<Object> r;
Cleaner c;
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;
}
}
// Cleaner繼承了PhantomReference,用于一些清理工作
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
// 有注冊隊列,未入隊
if (q != ReferenceQueue.NULL) q.enqueue(r);
3 引用類型
類型 | 強引用 | SoftReference |
WeakReference |
PhantomReference |
---|---|---|---|---|
定義 | 如果線程在不遍歷任何引用對象的情況下訪問某個對象,則該對象是強可達的。 新建的對象是線程強可達的。 |
如果對象不是強可達的,但可以通過遍歷軟引用來訪問,則該對象是軟可達的。 | 如果對象不是強、軟可達的,但可以通過遍歷弱引用訪問,那么它就是弱可達的。 當對弱可達對象的弱引用被清除時,就要對該對象執行finalization過程。 |
如果一個對象不是強、軟、弱可達的,那么它就是幻象可達的,對象已經被終止(finalized),但是幻象引用指向了它。 |
用途 | 普通引用 | 內存敏感的緩存 | map | 回收預清理(代替finalization機制) |
垃圾回收 | 不可達時 | 收集器根據內存情況進行回收,保證在OOM之前回收 |
referent 對象狀態會正常經歷finalizable 、finalized ,進而被回收 |
PhantomReference 在收集器確定其referent 不需要回收(maybe otherwise be reclaimed)時入隊 |
強度 | 依次減弱 | |||
是否注冊隊列 | - | 否(一般) | 否(一般) | 是 |
當對象不屬于上述四種可達方式時,成為不可達對象。不可達對象,需要進行回收。
軟引用代碼示例:
public static void main(String[] args) {
Reference reference = new SoftReference(new Object());
System.gc(); // 輸出結果非null
System.out.println("Object is: " + reference.get());
}
弱引用代碼示例:
public static void main(String[] args) {
Reference reference = new WeakReference(new Object());
System.gc(); // 注釋掉時,輸出結果非null
System.out.println("Object is: " + reference.get());
}
幻象引用代碼示例:
public static void main(String[] args) {
Reference reference = new PhantomReference(new Object(), null);
System.gc(); // 無論是否注釋掉,結果始終為null。因為重寫了get方法
System.out.println("Object is: " + reference.get());
}