好將來面試官:說說強引用、軟引用、弱引用、幻象引用有什麼區別?

前言

在Java語言中,除了原始數據類型的變量,其餘全部都是所謂的引用類型,指向各類不一樣的對象,理解引用對於掌握Java對象生命週期和JVM內部相關機制很是有幫助。java

今天我要問你的問題是,強引用、軟引用、弱引用、幻象引用有什麼區別?具體使用場景是什麼?面試

Java學習筆記共享地址:Java核心知識點200多頁學習筆記編程

不一樣的引用類型,主要體現的是對象不一樣的可達性(reachable)狀態和對垃圾收集的影響緩存

強引用

就是咱們最多見的普通對象引用,只要還有強引用指向一個對象,就能代表對象還「活着」,垃圾收集器不會碰這種對象。對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲null,就是能夠被垃圾收集的了,固然具體回收時機仍是要看垃圾收集策略。框架

軟引用

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

弱引用

並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就能夠用來構建一種沒有特定約束的關係,好比,維護一種非強制性的映射關係,若是試圖獲取時對象還在,就使用它,不然重現實例化。它一樣是不少緩存實現的選擇。異步編程

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

考點分析

這道面試題,屬於既偏門又很是高頻的一道題目。說它偏門,是由於在大多數應用開發中,不多直接操做各類不一樣引用,雖然咱們使用的類庫、框架可能利用了其機制。它被頻繁問到,是由於這是一個綜合性的題目,既考察了咱們對基礎概念的理解,也考察了對底層對象生命週期、垃圾收集機制等的掌握。學習

充分理解這些引用,對於咱們設計可靠的緩存等框架,或者診斷應用OOM等問題,會頗有幫助。好比,診斷MySQL connector-j驅動在特定模式下(useCompression=true)的內存泄漏問題,就須要咱們理解怎麼排查幻象引用的堆積問題。this

知識擴展

1.對象可達性狀態流轉分析

首先,請你看下面流程圖,我這裏簡單總結了對象生命週期和不一樣可達性狀態,以及不一樣狀態可能的改變關係,可能未必100%嚴謹,來闡述下可達性的變化。

我來解釋一下上圖的具體狀態,這是Java定義的不一樣可達性級別(reachability level),具體以下:

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

判斷對象可達性,是JVM垃圾收集器決定如何處理對象的一部分考慮。

全部引用類型,都是抽象類java.lang.ref.Reference的子類,你可能注意到它提供了get()方法:

除了幻象引用(由於get永遠返回null),若是對象尚未被銷燬,均可以經過get方法獲取原有對象。這意味着,利用軟引用和弱引用,咱們能夠將訪問到的對象,從新指向強引用,也就是人爲的改變了對象的可達性狀態!這也是爲何我在上面圖裏有些地方畫了雙向箭頭。

因此,對於軟引用、弱引用之類,垃圾收集器可能會存在二次確認的問題,以保證處於弱引用狀態的對象,沒有改變爲強引用。

可是,你以爲這裏有沒有可能出現什麼問題呢?

不錯,若是咱們錯誤的保持了強引用(好比,賦值給了static變量),那麼對象可能就沒有機會變回相似弱引用的可達性狀態了,就會產生內存泄漏。因此,檢查弱引用指向對象是否被垃圾收集,也是診斷是否有特定內存泄漏的一個思路,若是咱們的框架使用到弱引用又懷疑有內存泄漏,就能夠從這個角度檢查。

2.引用隊列(ReferenceQueue)使用

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

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

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

前面泛泛提到了引用對垃圾收集的影響,尤爲是軟引用,到底JVM內部是怎麼處理它的,其實並非很是明確。那麼咱們能不能使用什麼方法來影響軟引用的垃圾收集呢?

答案是有的。軟引用一般會在最後一次引用後,還能保持一段時間,默認值是根據堆剩餘空間計算的(以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已經逐步退出歷史舞臺。因此在咱們應用時,能夠參考相似設置,但不要過於依賴它。

4.診斷JVM引用狀況

若是你懷疑應用存在引用(或finalize)致使的回收問題,能夠有不少工具或者選項可供選擇,好比HotSpot JVM自身便提供了明確的選項(PrintReferenceGC)去獲取相關信息,我指定了下面選項去使用JDK 8運行一個樣例應用:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC

這是JDK 8使用ParrallelGC收集的垃圾收集日誌,各類引用數量很是清晰。

0.403: [GC (Allocation Failure) 0.871: [SoftReference, 0 refs, 0.0000393 secs]0.871: [WeakReference, 8 refs, 0.0000138 secs]0.871: [FinalReference, 4 refs, 0.0000094 secs]0.871: [PhantomReference, 0 refs, 0 refs, 0.0000085 secs]0.871: [JNI Weak Reference, 0.0000071 secs][PSYoungGen: 76272K->10720K(141824K)] 128286K->128422K(316928K), 0.4683919 secs] [Times: user=1.17 sys=0.03, real=0.47 secs]

注意:JDK 9對JVM和垃圾收集日誌進行了普遍的重構,相似PrintGCTimeStamps和PrintReferenceGC已經再也不存在,我在專欄後面的垃圾收集主題裏會更加系統的闡述。

5.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操做是徹底合法的。

new Resource().action()

相似的書寫結構,在異步編程中彷佛是很廣泛的,由於異步編程中每每不會用傳統的「執行->返回->使用」的結構。

在Java 9以前,實現相似功能相對比較繁瑣,有的時候須要採起一些比較隱晦的小技巧。幸虧,java.lang.ref.Reference給咱們提供了新方法,它是JEP 193: Variable Handles的一部分,將Java平臺底層的一些能力暴露出來:

static void reachabilityFence(Object ref)

在JDK源碼中,reachabilityFence大多使用在Executors或者相似新的HTTP/2客戶端代碼中,大部分都是異步調用的狀況。編程中,能夠按照上面這個例子,將須要reachability保障的代碼段利用try-finally包圍起來,在finally裏明確聲明對象強可達。

總結

今天,我總結了Java語言提供的幾種引用類型、相應可達狀態以及對於JVM工做的意義,並分析了引用隊列使用的一些實際狀況,最後介紹了在新的編程模式下,如何利用API去保障對象不被意外回收,但願對你有所幫助。

相關文章
相關標籤/搜索