Java基礎--強引用、軟引用、弱引用、幻象引用有什麼區別?

1.定義

強引用(“Strong” Reference),就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾收集器不會碰這種對象。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。

特點:我們平常典型編碼Object obj = new Object()中的obj就是強引用。通過關鍵字new創建的對象所關聯的引用就是強引用。 當JVM內存空間不足,JVM寧願拋出OutOfMemoryError運行時錯誤(OOM),使程序異常終止,也不會靠隨意回收具有強引用的“存活”對象來解決內存不足的問題。對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲 null,就是可以被垃圾收集的了,具體回收時機還是要看垃圾收集策略。

軟引用(SoftReference),是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

特點:軟引用通過SoftReference類實現。 軟引用的生命週期比強引用短一些。只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象:即JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。後續,我們可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的對象被回收。如果隊列爲空,將返回一個null,否則該方法返回隊列中前面的一個Reference對象。

應用場景:軟引用通常用來實現內存敏感的緩存。如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。

弱引用(WeakReference)並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就可以用來構建一種沒有特定約束的關係,比如,維護一種非強制性的映射關係,如果試圖獲取時對象還在,就使用它,否則重現實例化。它同樣是很多緩存實現的選擇。

弱引用通過WeakReference類實現。 弱引用的生命週期比軟引用短。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。由於垃圾回收器是一個優先級很低的線程,因此不一定會很快回收弱引用的對象。弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

應用場景:弱應用同樣可用於內存敏感的緩存。

幻象引用,有時候也翻譯成虛引用,你不能通過它訪問對象。幻象引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制,我在專欄上一講中介紹的 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的創建和銷燬。

特點:虛引用也叫幻象引用,通過PhantomReference類來實現。無法通過虛引用訪問對象的任何屬性或函數。幻象引用僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取一些程序行動。

應用場景:可用來跟蹤對象被垃圾回收器回收的活動,當一個虛引用關聯的對象被垃圾收集器回收之前會收到一條系統通知。

2.對象可達性分析

在這裏插入圖片描述

  • 強可達(Strongly Reachable),就是當一個對象可以有一個或多個線程可以不通過各種引用訪問到的情況。比如,我們新創建一個對象,那麼創建它的線程對它就是強可達。
  • 軟可達(Softly Reachable),就是當我們只能通過軟引用才能訪問到對象的狀態。
  • 弱可達(Weakly Reachable),類似前面提到的,就是無法通過強引用或者軟引用訪問,只能通過弱引用訪問時的狀態。這是十分臨近 finalize 狀態的時機,當弱引用被清除的時候,就符合 finalize 的條件了。
  • 幻象可達(Phantom Reachable),上面流程圖已經很直觀了,就是沒有強、軟、弱引用關聯,並且 finalize 過了,只有幻象引用指向這個對象的時候。
  • 當然,還有一個最後的狀態,就是不可達(unreachable),意味着對象可以被清除了。

3.Reference

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
所有引用類型,都是抽象類 java.lang.ref.Reference 的子類,你可能注意到它提供了 get() 方法:

除了幻象引用(因爲 get 永遠返回 null),如果對象還沒有被銷燬,都可以通過 get 方法獲取原有對象。這意味着,利用軟引用和弱引用,我們可以將訪問到的對象,重新指向強引用,也就是人爲的改變了對象的可達性狀態!這也是爲什麼我在上面圖裏有些地方畫了雙向箭頭。
所以,對於軟引用、弱引用之類,垃圾收集器可能會存在二次確認的問題,以保證處於弱引用狀態的對象,沒有改變爲強引用。
但是,你覺得這裏有沒有可能出現什麼問題呢?
不錯,如果我們錯誤的保持了強引用(比如,賦值給了 static 變量),那麼對象可能就沒有機會變回類似弱引用的可達性狀態了,就會產生內存泄漏。所以,檢查弱引用指向對象是否被垃圾收集,也是診斷是否有特定內存泄漏的一個思路,如果我們的框架使用到弱引用又懷疑有內存泄漏,就可以從這個角度檢查。

4.ReferenceQueue

我們在創建各種引用並關聯到相應對象時,可以選擇是否需要關聯引用隊列,JVM 會在特定時機將引用 enqueue 到隊列裏,我們可以從隊列裏獲取引用(remove 方法在這裏實際是有獲取的意思)進行相關後續邏輯。尤其是幻象引用,get 方法只返回 null,如果再不指定引用隊列,基本就沒有意義了。看看下面的示例代碼。利用引用隊列,我們可以在對象處於相應狀態時(對於幻象引用,就是前面說的被 finalize 了,處於幻象可達狀態),執行後期處理邏輯。


Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try {
    // Remove是一個阻塞方法,可以指定timeout,或者選擇一直阻塞
    Reference<Object> ref = refQueue.remove(1000L);
    if (ref != null) {
        // do something
    }
} catch (InterruptedException e) {
    // Handle it
}

5.顯式地影響軟引用垃圾收集

軟引用通常會在最後一次引用後,還能保持一段時間,默認值是根據堆剩餘空間計算的(以 M bytes 爲單位)。從 Java 1.3.1 開始,提供了 -XX:SoftRefLRUPolicyMSPerMB 參數,我們可以以毫秒(milliseconds)爲單位設置。比如,下面這個示例就是設置爲 3 秒(3000 毫秒)。

-XX:SoftRefLRUPolicyMSPerMB=3000

這個剩餘空間,其實會受不同 JVM 模式影響,對於 Client 模式,比如通常的 Windows 32 bit JDK,剩餘空間是計算當前堆裏空閒的大小,所以更加傾向於回收;而對於 server 模式 JVM,則是根據 -Xmx 指定的最大值來計算。
本質上,這個行爲還是個黑盒,取決於 JVM 實現,即使是上面提到的參數,在新版的 JDK 上也未必有效,另外 Client 模式的 JDK 已經逐步退出歷史舞臺。所以在我們應用時,可以參考類似設置,但不要過於依賴它。

6.診斷 JVM 引用情況

如果你懷疑應用存在引用(或 finalize)導致的回收問題,可以有很多工具或者選項可供選擇,比如 HotSpot JVM 自身便提供了明確的選項(PrintReferenceGC)去獲取相關信息.
注意:JDK 9 對 JVM 和垃圾收集日誌進行了廣泛的重構,類似 PrintGCTimeStamps 和 PrintReferenceGC 已經不再存在,我在專欄後面的垃圾收集主題裏會更加系統的闡述。

7.Reachability Fence

可以通過底層 API 來達到強引用的效果,這就是所謂的設置 reachability fence。
爲什麼需要這種機制呢?考慮一下這樣的場景,按照 Java 語言規範,如果一個對象沒有指向強引用,就符合垃圾收集的標準,有些時候,對象本身並沒有強引用,但是也許它的部分屬性還在被使用,這樣就導致詭異的問題,所以我們需要一個方法,在沒有強引用情況下,通知 JVM 對象是在被使用的。說起來有點繞,我們來看看 Java 9 中提供的案例。


class Resource {
 private static ExternalResource[] externalResourceArray = ...
 int myIndex; Resource(...) {
     myIndex = ...
     externalResourceArray[myIndex] = ...;
     ...
 }
 protected void finalize() {
     externalResourceArray[myIndex] = null;
     ...
 }
 public void action() {
 try {
     // 需要被保護的代碼
     int i = myIndex;
     Resource.update(externalResourceArray[i]);
 } finally {
     // 調用reachbilityFence,明確保障對象strongly reachable
     Reference.reachabilityFence(this);
 }
 }
 private static void update(ExternalResource ext) {
    ext.status = ...;
 }
} 

方法 action 的執行,依賴於對象的部分屬性,所以被特定保護了起來。否則,如果我們在代碼中像下面這樣調用,那麼就可能會出現困擾,因爲沒有強引用指向我們創建出來的 Resource 對象,JVM 對它進行 finalize 操作是完全合法的。
類似的書寫結構,在異步編程中似乎是很普遍的,因爲異步編程中往往不會用傳統的“執行 -> 返回 -> 使用”的結構。
在 Java 9 之前,實現類似功能相對比較繁瑣,有的時候需要採取一些比較隱晦的小技巧。幸好,java.lang.ref.Reference 給我們提供了新方法,它是 JEP 193: Variable Handles 的一部分,將 Java 平臺底層的一些能力暴露出來:
在這裏插入圖片描述
在 JDK 源碼中,reachabilityFence 大多使用在 Executors 或者類似新的 HTTP/2 客戶端代碼中,大部分都是異步調用的情況。編程中,可以按照上面這個例子,將需要 reachability 保障的代碼段利用 try-finally 包圍起來,在 finally 裏明確聲明對象強可達。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章