Java中各種引用(Reference)解析
目錄
- 1,引用型別
- 2, FinalReference
- 2.1, Finalizer
- 3, SoftReference
- 4, WeakReference
- 5, PhantomReference
- 6, ReferenceQueue
- 7,Cleaner
- 8, Reference
- 引用例項的幾個狀態
- 重點原始碼解析
- 總結
1,引用型別
java.lang.ref
整體包結構
型別 | 對應類 | 特徵 |
---|---|---|
強引用 | 強引用的物件絕對不會被gc回收 | |
軟引用 | SoftReference | 如果實體記憶體充足則不會被gc回收,如果實體記憶體不充足則會被gc回收。 |
弱引用 | WeakReference | 一旦被gc掃描到則會被回收 |
虛引用 | PhantomReference | 不會影響物件的生命週期,形同於無,任何時候都可能被gc回收 |
FinalReference | 用於收尾機制(finalization) |
2, FinalReference
FinalReference
訪問許可權為package,並且只有一個子類Finalizer
Finalizer
是final修飾的類,所以無法繼承擴充套件。
與Finalizer
相關聯的則是Object中的finalize()
方法,在類載入的過程中,如果當前類有覆寫finalize()
方法,則其物件會被標記為finalizer類,這種型別的物件被回收前會先呼叫其finalize()
。
具體的實現機制是,在gc進行可達性分析的時候,如果當前物件是finalizer型別的物件,並且本身不可達(與GC Roots無相連線的引用),則會被加入到一個ReferenceQueue
型別的佇列(F-Queue)中。而系統在初始化的過程中,會啟動一個FinalizerThread
例項的守護執行緒(執行緒名Finalizer),該執行緒會不斷消費F-Queue中的物件,並執行其finalize()
finalize()
方法的異常不會導致FinalizerThread
執行中斷退出。物件在執行finalize()
方法後,只是斷開了與Finalizer
的關聯,並不意味著會立即被回收,還是要等待下一次GC,而每個物件的finalize()
方法都只會執行一次,不會重複執行。
finalize()
方法是物件逃脫死亡命運的最後一次機會,如果在該方法中將物件本身(this關鍵字) 賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移出"即將回收的集合"。——《深入理解java虛擬機器》
注意:finalize()使用不當會導致記憶體洩漏和記憶體溢位,比如SocksSocketImpl
之類的服務會在finalize()
中加入close()
操作用於釋放資源,但是如果FinalizerThread
一直沒有執行的話就會導致資源一直無法釋放,從而出現記憶體洩漏。還有如果某物件的finalize()
方法執行時間太長或者陷入死迴圈,將導致F-Queue
一直堆積,從而造成記憶體溢位(oom)。
2.1, Finalizer
- FinalizerThread
//消費ReferenceQueue並執行對應元素物件的finalize()方法
private static class FinalizerThread extends Thread {
......
public void run() {
......
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
}
}
}
}
//初始化的時候啟動FinalizerThread(守護執行緒)
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
- add
在jvm啟動的時候就會啟動一個守護執行緒去消費引用佇列,並呼叫引用佇列指向物件的finalize()方法。
jvm在註冊finalize()方法被覆寫的物件的時候會建立一個Finalizer
物件,並且將該物件加入一個雙向連結串列中:
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
private void add() {
synchronized (lock) { //頭插法構建Finalizer物件的連結串列
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
另外還有兩個附加執行緒用於消費Finalizer連結串列以及佇列:
Runtime.runFinalization()
會呼叫runFinalization()
用於消費Finalizer佇列,而java.lang.Shutdown
則會在jvm退出的時候(jvm關閉鉤子)呼叫runAllFinalizers()
用於消費Finalizer連結串列。
3, SoftReference
系統將要發生記憶體溢位(oom)之前,會回收軟引用的物件,如果回收後還沒有足夠的記憶體,丟擲記憶體溢位異常;
使用SoftReference類,將要軟引用的物件最為引數傳入;
構造方法傳入ReferenceQueue佇列的時候,如果引用的物件被回收,則將其加入該佇列。
public SoftReference(T referent) 根據傳入的引用建立軟引用
public SoftReference(T referent, ReferenceQueue<? super T> q)根據傳入的引用和註冊佇列建立軟引用
使用示例:
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
SoftReference<String> softReference = new SoftReference<>("abc", referenceQueue);
System.gc();
System.out.println(softReference.get());
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference);
執行結果如下:
abc
null
軟引用可用來實現記憶體敏感的快取記憶體
4, WeakReference
WeakReference
與SoftReference
類似,區別在於WeakReference
的生命週期更短,一旦發生GC就會被回收,不過由於gc的執行緒優先順序比較低,所以WeakReference
不會很快被GC發現並回收。
使用WeakReference
類,將要弱引用的物件最為引數傳入;
構造方法傳入ReferenceQueue佇列的時候,如果引用的物件被回收,則將其加入該佇列。
WeakReference(T referent) 根據傳入的引用建立弱引用
WeakReference(T referent, ReferenceQueue<? super T> q) 根據傳入的引用和註冊佇列建立弱引用
使用示例:
public class WeakReferenceTest {
public static void main(String[] args) {
ReferenceQueue<String> rq = new ReferenceQueue<>();
//這裡必須用new String構建字串,而不能直接傳入字面常量字串
Reference<String> r = new WeakReference<>(new String("java"), rq);
Reference rf;
//一次System.gc()並不一定會回收A,所以要多試幾次
while((rf=rq.poll()) == null) {
System.gc();
}
System.out.println(rf);
if (rf != null) {
//引用指向的物件已經被回收,存入引入佇列的是弱引用本身,所以這裡最終返回null
System.out.println(rf.get());
}
}
}
執行結果:
java.lang.ref.WeakReference@5a07e868
null
5, PhantomReference
虛引用是引用中最弱的引用型別,有些形同虛設的意味。不同於軟引用和弱引用,虛引用不會影響物件的生命週期,如果一個物件僅持有虛引用,那麼它就相當於無引用指向,不可達,被gc掃描到就會被回收,虛引用無法通過get()方法來獲取目標物件的強引用從而使用目標物件,虛引用中get()方法永遠返回null。
虛引用必須和引用佇列(ReferenceQueue)聯合使用,當gc回收一個被虛引用指向的物件時,會將虛引用加入相關聯的引用佇列中。虛引用主要用於追蹤物件gc回收的活動,通過檢視引用佇列中是否包含物件所對應的虛引用來判斷它是否即將被回收。
虛引用的一個應用場景是用來追蹤gc回收對應物件的活動。
public PhantomReference(T referent, ReferenceQueue<? super T> q) 建立弱引用
示例:
public class PhantomReferenceTest {
public static void main(String[] args) {
ReferenceQueue<String> rq = new ReferenceQueue<>();
PhantomReference<String> reference = new PhantomReference<>(new String("cord"), rq);
System.out.println(reference.get());
System.gc();
System.runFinalization();
System.out.println(rq.poll() == reference);
}
}
執行結果:
null
true
6, ReferenceQueue
ReferenceQueue內部資料結構是一個連結串列,連結串列裡的元素是加入進去的Reference例項,然後通過wait
和notifyAll
與物件鎖實現生產者和消費者,通過這種方式模擬一個佇列。
ReferenceQueue是使用wati()和notifyAll()實現生產者和消費者模式的一個具體場景。
ReferenceQueue重點原始碼解析:
- NULL和ENQUEUED
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
這兩個靜態屬性主要用於標識加入引用佇列的引用的狀態,NULL
標識該引用已被當前佇列移除過,ENQUEUED
標識該引用已加入當前佇列。
- enqueue(Reference<? extends T> r)
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (lock) {
//檢查該引用是否曾從當前佇列移除過或者已經加入當前隊列了,如果有則直接返回
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
r.queue = ENQUEUED;//將引用關聯的佇列統一標識為ENQUEUED
r.next = (head == null) ? r : head;//當前引用指向head
head = r; //將head指向當前引用(連結串列新增節點採用頭插法)
queueLength++; //更新連結串列長度
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1); //
}
lock.notifyAll(); //通知消費端
return true;
}
}
- remove(long timeout)
remove嘗試移除佇列中的頭部元素,如果佇列為空則一直等待直至達到指定的超時時間。
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; //如果成功移除則直接返回
long start = (timeout == 0) ? 0 : System.nanoTime();
for (;;) {
lock.wait(timeout); //釋放當前執行緒鎖,等待notify通知喚醒
r = reallyPoll();
if (r != null) return r;
if (timeout != 0) { //如果超時時間不為0則校驗超時
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
if (timeout <= 0) return null; //如果剩餘時間小於0則返回
start = end;
}
}
}
}
7,Cleaner
Cleaner是PhantomReference
的一個子類實現,提供了比finalization(收尾機制)
更輕量級和健壯的實現,因為Cleaner中的清理邏輯是由Reference.ReferenceHandler
直接呼叫的,而且由於是虛引用的子類,它完全不會影響指向的物件的生命週期。
一個Cleaner例項記錄了一個物件的引用,以及一個包含了清理邏輯的Runnable例項。當Cleaner指向的引用被gc回收後,Reference.ReferenceHandler
會不斷消費引用佇列中的元素,當元素為Cleaner型別的時候就會呼叫其clean()方法。
Cleaner不是用來替代finalization的,只有在清理邏輯足夠輕量和直接的時候才適合使用Cleaner,繁瑣耗時的清理邏輯將有可能導致ReferenceHandler執行緒阻塞從而耽誤其它的清理任務。
重點原始碼解析:
public class Cleaner extends PhantomReference<Object>
{
//一個統一的空佇列,用於虛引用構造方法,Cleaner的trunk會被直接呼叫不需要通過佇列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
//Cleaner內部為雙向連結串列,防止虛引用本身比它們引用的物件先被gc回收,此為頭節點
static private Cleaner first = null;
//新增節點
private static synchronized Cleaner add(Cleaner cl) {
if (first != null) { //頭插法加入節點
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}
//移除節點
private static synchronized boolean remove(Cleaner cl) {
//指向自己說明已經被移除
if (cl.next == cl)
return false;
//移除頭部節點
if (first == cl) {
if (cl.next != null)
first = cl.next;
else
first = cl.prev;
}
if (cl.next != null)//下一個節點指向前一個節點
cl.next.prev = cl.prev;
if (cl.prev != null)//前一個節點指向下一個節點
cl.prev.next = cl.next;
//自己指向自己標識已被移除
cl.next = cl;
cl.prev = cl;
return true;
}
//清理邏輯runnable實現
private final Runnable thunk;
...
//呼叫清理邏輯
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
...
}
}
}
Cleaner可以用來實現對堆外記憶體進行管理,DirectByteBuffer
就是通過Cleaner實現堆外記憶體回收的:
DirectByteBuffer(int cap) { //構造方法中建立引用物件相關聯的Cleaner物件
...
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
private static class Deallocator implements Runnable {
...
public void run() { //記憶體回收的邏輯(具體實現參看原始碼此處不展開)
...
}
}
8, Reference
Reference是上面列舉的幾種引用包括Cleaner的共同父類,一些引用的通用處理邏輯均在這裡面實現。
引用例項的幾個狀態
Active
當處於Active狀態,gc會特殊處理引用例項,一旦gc檢測到其可達性發生變化,gc就會更改其狀態。此時分兩種情況,如果該引用例項建立時有註冊引用佇列,則會進入pending狀態,否則會進入inactive狀態。新建立的引用例項為Active。
Pending
當前為pending-Reference列表中的一個元素,等待被ReferenceHandler執行緒消費並加入其註冊的引用佇列。如果該引用例項未註冊引用佇列,則永遠不會處理這個狀態。
Enqueued
該引用例項建立時有註冊引用佇列並且當前處於入佇列狀態,屬於該引用佇列中的一個元素。當該引用例項從其註冊引用佇列中移除後其狀態變為Inactive。如果該引用例項未註冊引用佇列,則永遠不會處理這個狀態。
Inactive
當處於Inactive狀態,無需任何處理,一旦變成Inactive狀態則其狀態永遠不會再發生改變。
整體遷移流程圖如下:
重點原始碼解析
1,Reference中的幾個關鍵屬性
//關聯的物件的引用,根據引用型別不同gc針對性處理
private T referent;
//引用註冊的佇列,如果有註冊佇列則回收引用會加入該佇列
volatile ReferenceQueue<? super T> queue;
//上面引用佇列referenceQueue中儲存引用的連結串列
/* active: NULL //未加入佇列前next指向null
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
Reference next;
/* When active: 由gc管理的引用發現連結串列的下一個引用
* pending: pending連結串列中的下一個元素
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
/*
*等待入佇列的引用連結串列,gc往該連結串列加引用物件,Reference-handler執行緒消費該連結串列。
* 它通過discovered連線它的元素
*/
private static Reference<Object> pending = null;
2,ReferenceHandler
private static class ReferenceHandler extends Thread {
...
public void run() {
while (true) {
tryHandlePending(true); //無限迴圈呼叫tryHandlePending
}
}
}
static {
... jvm啟動時以守護執行緒執行ReferenceHandler
Thread handler = new ReferenceHandler(tg, "Reference Handler");
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
//註冊JavaLangRefAccess匿名實現,堆外記憶體管理會用到(Bits.reserveMemory)
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
//消費pending佇列
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...
//判斷是否為Cleaner例項
c = r instanceof Cleaner ? (Cleaner) r : null;
//將r從pending連結串列移除
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
//如果pending沒有元素可消費則等待通知
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
//釋放cpu資源
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
//呼叫Cleaner清理邏輯(可參考前面的7,Cleaner段落)
if (c != null) {
c.clean();
return true;
}
//如果當前引用例項有註冊引用佇列則將其加入引用佇列
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
總結
jvm中引用有好幾種類型的實現,gc針對這幾種不同型別的引用有著不同的回收機制,同時它們也有著各自的應用場景, 比如SoftReference可以用來做快取記憶體, WeakReference也可以用來做一些普通快取(WeakHashMap), 而PhantomReference則用在一些特殊場景,比如Cleaner就是一個很好的應用場景,它可以用來回收堆外記憶體。與此同時,SoftReference, WeakReference, PhantomReference這幾種弱型別引用還可以與引用佇列結合使用,使得可以在關聯引用回收之後可以做一些額外處理,甚至於Finalizer(收尾機制)都可以在物件回收過程中改變物件的生命週期。
參考連結:
https://www.ibm.com/developerworks/cn/java/j-fv/index.html
https://www.infoq.cn/article/jvm-source-code-analysis-finalreference
https://www.ibm.com/developerworks/cn/java/j-lo-langref/index.html
https://www.cnblogs.com/duanxz/p/10275778.html
《深入理解java虛擬機器》
https://blog.csdn.net/mazhimazh/article/details/19752475
https://www.tuicool.com/articles/AZ7Fvqb
https://blog.csdn.net/aitangyong/article/details/39455229
https://www.cnblogs.com/duanxz/p/6089485.html
https://www.throwable.club/2019/02/16/java-reference/#Reference的狀態集合
http://imushan.com/2018/08/19/java/language/JDK%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-Refere