Java中各種引用(Reference)解析
- 2019 年 10 月 3 日
- 筆記
目錄
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()
方法(runFinalizer),並且runFinalizer方法會捕獲Throwable級別的異常,也就是說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-Reference/