探究Java中的引用
- 2020 年 3 月 3 日
- 筆記
探究Java中的四種引用
從JDK1.2版本開始,Java把對象的引用分為四種級別,從而使程式能更加靈活的控制對象的生命周期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。本篇就來詳細探究一下這四種引用的機制:
- 強引用
- 軟引用
- 弱引用
- 虛引用
- 詳解ReferenceQueue與Reference
強引用
強引用是最普遍的引用,一般通過new關鍵字來創建出來的對象引用都屬於強引用,比如Object o = new Object()。
如果一個對象具有強引用,它就不會被垃圾回收器回收。即使當前記憶體空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程式異常終止。如果想中斷強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。
軟引用
在使用軟引用時,如果記憶體的空間足夠,軟引用就能繼續被使用,而不會被垃圾回收器回收;只有在記憶體空間不足時,軟引用才會被垃圾回收器回收。軟引用最長被用作記憶體敏感型的記憶體快取。
創建一個軟引用的程式碼示例:
1 |
SoftReference<String> soft = new SoftReference<>("World");
|
SoftReference類的結構如下:
1 2 3 4 5 6 7 8 9 |
java.lang.ref.SoftReference#SoftReference(T) java.lang.ref.SoftReference#SoftReference(T, java.lang.ref.ReferenceQueue<? super T>) java.lang.ref.SoftReference#get java.lang.ref.SoftReference#clock java.lang.ref.SoftReference#timestamp
|
- 前兩個是構造函數,後面會詳細介紹ReferenceQueue;
- get方法用於獲取這個軟引用所指向的對象,如果這個對象已經清除或者被GC收集,那麼就返回null;
- clock屬性是一個static的long型時間戳,由GC執行緒進行GC的時候更新;
- timestamp屬性也是一個時間戳,每次在調用get方法的時候會對其進行更新,JVM用這個屬性用於幫助收集和清理軟引用。
弱引用
如果一個對象只具有弱引用,當 JVM 進行垃圾回收時,只要GC執行緒檢測到了,無論當前記憶體空間是否充足,都會將其回收。不過由於垃圾回收器是一個優先順序較低的執行緒,所以並不一定能迅速發現弱引用對象。弱引用通常用於實現規範化映射。
創建一個弱引用的程式碼示例:
1 |
WeakReference<String> weakName = new WeakReference<String>("hello");
|
WeakReference類的結構如下:
1 2 3 |
java.lang.ref.WeakReference#WeakReference(T) java.lang.ref.WeakReference#WeakReference(T, java.lang.ref.ReferenceQueue<? super T>)
|
只是提供了兩個構造函數,後一個構造函數傳入了一個ReferenceQueue對象。
虛引用
顧名思義,就是形同虛設的引用,如果一個對象僅持有虛引用,那麼它相當於沒有引用,在任何時候都可能被垃圾回收器回收。
創建虛引用的程式碼示例:
1 2 3 |
ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
|
PhantomReference類的結構如下:
1 2 3 |
java.lang.ref.PhantomReference#PhantomReference(T, ReferenceQueue<? super T>) java.lang.ref.PhantomReference#get
|
- 只有一個帶有ReferenceQueue參數的構造函數,也就是虛引用一定要和引用隊列一起使用;
- 同時其get方法也有點特殊,因為虛引用的引用對象相當於沒有引用,所以其get方法總是返回null。
詳解ReferenceQueue與Reference
引用隊列可以與軟引用、弱引用以及虛引用一起配合使用,當垃圾回收器準備回收一個對象時,如果發現它還有引用,那麼就會在回收對象之前,把這個引用加入到與之關聯的引用隊列中去。程式可以通過判斷引用隊列中是否已經加入了引用,來判斷被引用的對象是否將要被垃圾回收,這樣就可以在對象被回收之前採取一些必要的措施。
與軟引用、弱引用不同,虛引用必須和引用隊列一起使用。
ReferenceQueue實現了一個隊列的入隊(enqueue)和出隊(poll還有remove)操作,內部元素就是Reference。ReferenceQueue名義上是一個隊列,但內部並沒有實際的存儲結構,它的存儲是依賴於內部節點之間的關係來實現的。看一下ReferenceQueue實現中用到的屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static ReferenceQueue<Object> NULL = new Null<>(); static ReferenceQueue<Object> ENQUEUED = new Null<>(); static private class Lock { }; private Lock lock = new Lock(); private volatile Reference<? extends T> head = null; private long queueLength = 0;
|
其實就是一個增加了同步操作的鏈表的設計,通過head屬性來找到鏈表頭,每個鏈表節點,即Reference對象,都有一個next屬性來找到下一個節點。
剛才分析四種引用的時候看到,java.lang.ref.Reference 為 軟(soft)引用、弱(weak)引用、虛(phantom)引用的父類。那我們再來看一下Reference類的實現中用到的屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private T referent; /* Treated specially by GC */ volatile ReferenceQueue<? super T> queue; volatile Reference next; transient private Reference<T> discovered; /* used by VM */ static private class Lock { } private static Lock lock = new Lock(); private static Reference<Object> pending = null;
|
Reference作為ReferenceQueue中的節點,定義了next屬性來指向下一個節點,referent為實際指向的對象,pending存儲等待被放入ReferenceQueue的引用對象;discovered表示要處理的下一個對象。
Reference類還定義了一個ReferenceHandler執行緒。
1 2 3 4 5 |
java.lang.ref.Reference.ReferenceHandler#ReferenceHandler java.lang.ref.Reference.ReferenceHandler#ensureClassInitialized java.lang.ref.Reference.ReferenceHandler#run
|
這個執行緒在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對象管理之間的一個消息傳遞方式,它使得我們可以對所監聽的對象引用可達發生變化時做一些處理。