【JVM從小白學成大佬】3.深入解析強引用、軟引用、弱引用、幻象引用
關於強引用、軟引用、弱引用、幻象引用的區別,在很多公司的面試題中經常出現,可能有些小夥伴覺得這個知識點比較冷門,但其實大家在開發中經常用到,如new一個物件的時候就是強引用的應用。
在java語言中,除了原始資料型別(boolean、byte、short、char、int、float、double、long)的變數,其他所有都是所謂的引用型別,指向各種不同的物件。理解這些引用的區別,對於掌握java物件生命週期和JVM內部相關機制非常有幫助。也有助於更深刻的理解底層物件生命週期、垃圾收集機制等,對設計可靠的快取框架、診斷應用OOM等問題也大有裨益。
這四種應用主要的區別體現在物件不同的可達性狀態和對垃圾收集的影響,他們之間的可達性狀態可以參看下圖:
1.強引用(strong reference)
強引用就是我們最常見的普通物件引用(如new 一個物件),只要還有強引用指向一個物件,就表明此物件還“活著”。在強引用面前,即使JVM記憶體空間不足,JVM寧願丟擲OutOfMemoryError執行時錯誤(OOM),讓程式異常終止,也不會靠回收強引用物件來解決記憶體不足的問題。對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就意味著此物件可以被垃圾收集了。但要注意的是,並不是賦值為null後就立馬被垃圾回收,具體的回收時機還是要看垃圾收集策略的。
如Object obj = new Object();
2.軟引用(soft reference)
軟引用相對強引用要弱化一些,可以讓物件豁免一些垃圾收集。當記憶體空間足夠的時候,垃圾回收器不會回收它。只有當JVM認定記憶體空間不足時才會去回收軟引用指向的物件。JVM會確保在丟擲OOM前清理軟引用指向的物件,而且JVM是很聰明的,會盡可能優先回收長時間閒置不用的軟引用指向的物件,對那些剛構建的或剛使用過的軟引用指向的物件儘可能的保留。基於軟引用的這些特性,軟引用可以用來實現很多記憶體敏感點的快取場景,即如果記憶體還有空閒,可以暫時快取一些業務場景所需的資料,當記憶體不足時就可以清理掉,等後面再需要時,可以重新獲取並再次快取。這樣就確保在使用快取提升效能的同時,不會導致耗盡記憶體。
軟引用通常可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
//有時候會返回null
sf.get();
通過上面的程式碼可以看出sf是對obj的一個軟引用,當sf物件還沒有被銷燬前,sf.get()可以獲取到這個物件,如果已被銷燬,則返回null。
正確使用軟引用的示例程式碼如下:
SoftReference<List<Foo>> ref = new SoftReference<List<Foo>>(new LinkedList<Foo>());
// somewhere else in your code, you create a Foo that you want to add to the list
List<Foo> list = ref.get();
if (list != null)
{
list.add(foo);
}
else
{
// list is gone; do whatever is appropriate
}
在使用軟引用的時候必須檢查引用是否為null。因為垃圾收集器可能在任意時刻回收軟引用,如果不做是否null的判斷,可能會出現NullPointerException的異常。
總的來說,軟引用是用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
3.弱引用(weak reference)
弱引用指向的物件是一種十分臨近finalize狀態的情況,當弱引用被清除的時候,就符合finalize的條件了。弱引用與軟引用最大的區別就是弱引用比軟引用的生命週期更短暫。垃圾回收器會掃描它所管轄的記憶體區域的過程中,只要發現弱引用的物件,不管記憶體空間是否有空閒,都會立刻回收它。如同前面我說過的,具體的回收時機還是要看垃圾回收策略的,因此那些弱引用的物件並不是說只要達到弱引用狀態就會立馬被回收。
基於弱引用的這些特性,弱引用同樣可以應用在很多需要快取的場景。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
//有時候會返回null
wf.get();
//返回是否被垃圾回收器標記為即將回收的垃圾
wf.isEnQueued();
4.幻象引用(phantom reference)
幻象引用,也有被說成是虛引用或幽靈引用。幻象引用並不會決定物件的生命週期。即如果一個物件僅持有虛引用,就相當於沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。不能通過它訪問物件,幻象引用僅僅是提供了一種確保物件被finalize以後,做某些事情的機制(如做所謂的Post-Mortem清理機制),也有人利用幻象引用監控物件的建立和銷燬。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
//永遠返回null
pf.get();
//返回是否從記憶體中已經刪除
pf.isEnQueued();
幻象引用的get方法永遠返回null,主要用於檢查物件是否已經從記憶體中刪除。
5.生存還是死亡
通過上面對四種引用型別的分析,你可能發現有些物件即使不可達,但也並非是“非死不可”的,這個時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。
如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,並在稍後被一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統奔潰。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可。譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。
任何一個物件的finalize()方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的finalize()方法不會被再次執行。
6.總結
物件的可達性是JVM垃圾收集器決定如何處理物件的一個重要考慮指標。
所有引用型別都是抽象類java.lang.ref.Reference的子類,子類裡提供了get()方法。通過上面的分析中可以得知,除了幻象引用(因為get永遠返回null),如果物件還沒有被銷燬,都可以通過get方法獲取原有物件。其實有個非常關鍵的注意點,利用軟引用和弱引用,我們可以將訪問到的物件,重新指向強引用,也就是人為的改變了物件的可達性狀態。所以對於軟引用、弱引用之類,垃圾收集器可能會存在二次確認的問題,以確保處於弱引用狀態的物件沒有改變為強引用。
但是有個問題,如果我們錯誤的保持了強引用(比如,賦值給了static變數),那麼物件可能就沒有機會變回類似弱引用的可達性狀態了,就會產生記憶體洩露。所以,檢查弱引用指向物件是否被垃圾收集,也是診斷是否有特定記憶體洩露的一個思路,我們的框架使用到弱引用又懷疑有記憶體洩露,就可以從這個角度檢查。
對於軟引用、弱引用、幻象引用可以配合引用佇列(ReferenceQueue)來使用,特別是幻象引用,get方法只返回null,如果再不指定引用佇列,基本就沒有任何意義了。
上面分析了四種引用型別的使用,熟悉這幾種應用型別對深入理解JVM也大有裨益。
熱門閱讀:
【JVM從小白學成大佬】1.開篇
【JVM從小白學成大佬】2.Java虛擬機器執行時資料區
參考:
《深入理解Java虛擬機器》
http://www.kdgregory.com/index.php?page=java.ref