Java中的四種(強、軟、弱和虛)引用

一、強引用(StrongReference)

強引用很常見,其寫法如下:

StringBuffer buffer = new StringBuffer();

上面創(chuàng)建了一個 StringBuffer 對象,并將這個對象的(強)引用存到變量 buffer 中。強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的交互。具體來說,如果一個對象通過一串強引用鏈接可到達(Strongly reachable),它是不會被回收的。如果不想讓正在使用的對象被回收,這正是所需要的。

但是強引用如此之強,在一個程序里,將一個類設置成不可被擴展是有點不太常見的,當然這個完全可以通過類標記成 final 實現(xiàn)?;蛘咭部梢愿訌碗s一些,就是通過內部包含了未知數(shù)量具體實現(xiàn)的工廠方法返回一個接口(Interface)。舉個例子,想要使用一個叫做 Zoo 的類,但是這個類不能被繼承,所以無法增加新的功能。

但是想追蹤 Zoo 對象的額外信息,該怎么辦? 假設需要記錄每個對象的序列號,但是由于 Zoo 類并不包含這個屬性,而且也不能擴展導致也不能增加這個屬性。其實一點問題也沒有,HashMap 完全可以解決上述的問題。

serialNumberMap.put(zoo, zooSerialNumber);

表面看上去沒問題,但是 zoo 對象的強引用很有可能會引發(fā)問題??梢源_信當一個 zoo 序列號不需要時,應該將這個條目從 map 中移除。如果沒有移除的話,可能會導致內存泄露,亦或者手動移除時刪除了正在使用的 zoo,會導致有效數(shù)據的丟失。其實這些問題很類似,這就是沒有垃圾回收機制的語言管理內存時常遇到的問題。

另一個強引用可能帶來的問題就是緩存,尤其是像圖片這樣的大文件的緩存。假設有一個程序需要處理用戶提供的圖片,通常的做法就是做圖片數(shù)據緩存,因為從磁盤加載圖片代價很大,并且同時也想避免在內存中同時存在兩份一樣的圖片數(shù)據。

緩存被設計的目的就是避免去再次加載哪些不需要的文件。你會很快發(fā)現(xiàn)在緩存中會一直包含一個到已經指向內存中圖片數(shù)據的引用。使用強引用會強制圖片數(shù)據留在內存,這就需要決定什么時候圖片數(shù)據不需要并且手動從緩存中移除,進而可以讓垃圾回收器回收。因此再一次被強制做垃圾回收器該做的工作,并且人為決定是該清理到哪一個對象。

二、軟引用(SoftReference)

軟引用基本上和弱引用差不多,只是相比弱引用,它阻止垃圾回收器回收其指向的對象的能力強一些。如果一個對象是弱引用可到達,那么這個對象會被垃圾回收器接下來的回收周期銷毀。但是如果是軟引用可到達,那么這個對象會停留在內存更時間上長一些。當內存不足時垃圾回收器才會回收這些軟引用可到達的對象。

由于軟引用可到達的對象比弱引用可達到的對象滯留內存時間會長一些,可以利用這個特性來做緩存。這樣的話,就可以節(jié)省很多事情,垃圾回收器會關心當前哪種可到達類型以及內存的消耗程度來進行處理。

三、弱引用(WeakReference)

Java 中的弱引用具體指的是java.lang.ref.WeakReference<T>類,以下是官方文檔對它做的說明:
弱引用對象的存在不會阻止它所指向的對象被垃圾回收器回收。弱引用最常見的用途是實現(xiàn)規(guī)范映射(canonicalizing mappings,比如哈希表)。
假設垃圾回收器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾回收器會清除所有指向該對象的弱引用,然后把這個弱可達對象標記為可終結(finalizable)的,這樣它隨后就會被回收。與此同時或稍后,垃圾回收器會把那些剛清除的弱引用放入創(chuàng)建弱引用對象時所指定的引用隊列(Reference Queue)中。

弱引用簡單來說就是將對象留在內存的能力不是那么強的引用。使用 WeakReference,垃圾回收器會決定引用的對象何時回收并且將對象從內存移除。創(chuàng)建弱引用寫法如下:

WeakReference<Zoo> weakZoo = new WeakReference<Zoo>(zoo);

使用 weakZoo.get() 就可以得到真實的 Zoo 對象。因為弱引用不能阻擋垃圾回收器對其回收,當沒有任何強引用到 zoo 對象時,使用 get 返回 null。
解決上述的 zoo 序列數(shù)記錄的問題,最簡單的辦法就是使用 Java 內置的 WeakHashMap 類。WeakHashMap 和 HashMap 幾乎一樣,唯一的區(qū)別就是它的鍵(不是值!!!)使用 WeakReference。當 WeakHashMap 的鍵標記為垃圾的時候,這個鍵對應的條目就會自動被移除。這就避免了上面不需要的 Zoo 對象手動刪除的問題。使用 WeakHashMap 可以很便捷地轉為 HashMap 或者 Map。

一旦弱引用對象開始返回 null,該弱引用指向的對象就被標記成了垃圾。而這個弱引用對象(非其指向的對象)就沒用了。通常這時候需要進行一些清理工作。比如 WeakHashMap 會在這時候移除沒用的條目來避免保存無限制增長的沒有意義的弱引用。

引用隊列可以很容易地實現(xiàn)跟蹤不需要的引用。當在構造 WeakReference 時傳入一個 ReferenceQueue 對象,當該引用指向的對象被標記為垃圾的時候,這個引用對象會自動地加入到引用隊列里面。接下來,就可以在固定的周期,處理傳入的引用隊列,比如做一些清理工作來處理這些沒有用的引用對象。

理解:為什么使用弱引用?

現(xiàn)有一個熊貓類 Pandas,該類被設計為不可擴展的,此時想要為每只熊貓增加一個編號。一種解決方案是使用 HashMap<Pandas, Integer>。問題來了,如果已經不再需要一個 Pandas 對象存在于內存中(比如已經賣出了這件產品),假設指向它的引用為 pandaA,這時會給 pandaA 賦值為 null,然而 pandaA 過去指向的 Pandas 對象并不會被回收,因為它顯然還被 HashMap 引用著。所以這種情況下,想要真正的回收一個 Pandas 對象,僅僅把它的強引用賦值為 null 是不夠的,還要把相應的條目從 HashMap 中移除?!皬?HashMap 中移除不再需要的條目”開發(fā)者必須附帶該操作,然還是希望垃圾回收器知曉:在只有 HashMap 中的 key 在引用著 Pandas 對象的情況下,就可以回收相應的 Pandas 對象了。顯然,使用弱引用能達成這個目的。只需要用一個指向 Pandas 對象的弱引用對象來作為 HashMap 中的 key 就可以了。

如何使用弱引用?

拿上面介紹的場景舉例,使用一個指向 Pandas 對象的弱引用對象來作為 HashMap的key,如下定義該弱引用對象:

Pandas pandaA = new Pandas(...);
WeakReference<Pandas> weakPandaA = new WeakReference<>(pandaA);

現(xiàn)在,弱引用對象 weakPandaA 就指向了 Pandas 對象 pandaA。怎么通過 weakPandaA 獲取它所指向的 Pandas 對象 pandaA 呢?很簡單,只需要下面這句代碼:

Pandas pandas= weakPandaA.get();

實際上,Java 類庫提供了 WeakHashMap 類,使用該類,它的鍵自然就是弱引用對象,無需再手動包裝原始對象。這樣一來,當 pandaA 變?yōu)?null 時(表明它所引用的 Pandas 已經無需存在于內存中),這時指向該 Pandas 對象的就是弱引用對象 weakPandaA 了,顯然此時相應的 Pandas 對象是弱可達的,所以指向它的弱引用會被清除,該 Pandas 對象隨即會被回收,指向它的弱引用對象會進入引用隊列中。

四、虛引用 (PhantomReference)

與軟引用,弱引用不同,虛引用指向的對象十分脆弱,我們不可以通過get方法來得到其指向的對象。它的唯一作用就是當其指向的對象被回收之后,自己被加入到引用隊列,用作記錄該引用指向的對象已被銷毀。

當弱引用的指向對象變得弱引用可到達,該弱引用就會加入到引用隊列。這一操作發(fā)生在對象析構或者垃圾回收真正發(fā)生之前。理論上,這個即將被回收的對象是可以在一個不符合規(guī)范的析構方法里面重新復活。但是這個弱引用會銷毀。虛引用只有在其指向的對象從內存中移除掉之后才會加入到引用隊列中。其get方法一直返回null就是為了阻止其指向的幾乎被銷毀的對象重新復活。

虛引用使用場景主要由兩個。它允許你知道具體何時其引用的對象從內存中移除。而實際上這是Java中唯一的方式。這一點尤其表現(xiàn)在處理類似圖片的大文件的情況。當你確定一個圖片數(shù)據對象應該被回收,你可以利用虛引用來判斷這個對象回收之后再繼續(xù)加載下一張圖片。這樣可以盡可能地避免可怕的內存溢出錯誤。

第二點,虛引用可以避免很多析構時的問題。finalize方法可以通過創(chuàng)建強引用指向快被銷毀的對象來讓這些對象重新復活。然而,一個重寫了finalize方法的對象如果想要被回收掉,需要經歷兩個單獨的垃圾回收周期。在第一個周期中,某個對象被標記為可回收,進而才能進行析構。但是因為在析構過程中仍有微弱的可能這個對象會重新復活。這種情況下,在這個對象真實銷毀之前,垃圾回收器需要再次運行。因為析構可能并不是很及時,所以在調用對象的析構之前,需要經歷數(shù)量不確定的垃圾回收周期。這就意味著在真正清理掉這個對象的時候可能發(fā)生很大的延遲。這就是為什么當大部分堆被標記成垃圾時還是會出現(xiàn)煩人的內存溢出錯誤。

使用虛引用,上述情況將引刃而解,當一個虛引用加入到引用隊列時,你絕對沒有辦法得到一個銷毀了的對象。因為這時候,對象已經從內存中銷毀了。因為虛引用不能被用作讓其指向的對象重生,所以其對象會在垃圾回收的第一個周期就將被清理掉。

顯而易見,finalize方法不建議被重寫。因為虛引用明顯地安全高效,去掉finalize方法可以虛擬機變得明顯簡單。當然你也可以去重寫這個方法來實現(xiàn)更多。這完全看個人選擇。

五、引用隊列(Reference Queue)

弱引用類(WeakReference)有兩個構造函數(shù):

//創(chuàng)建一個指向給定對象的弱引用
WeakReference(T referent) 
//創(chuàng)建一個指向給定對象并且登記到給定引用隊列的弱引用
WeakReference(T referent, ReferenceQueue<? super T> q) 

可以看到第二個構造方法中提供了一個 ReferenceQueue 類型的參數(shù),通過提供該參數(shù),便把創(chuàng)建的弱引用對象注冊到了一個引用隊列上,這樣當它被垃圾回收器清除時,就會把它送入這個引用隊列中,便可以對這些被清除的弱引用對象進行統(tǒng)一管理。

六、總結

  1. 強引用(StrongReference):通常通過 new 來創(chuàng)建一個新對象時返回的引用就是一個強引用,若一個對象通過一系列強引用可到達,它就是強可達的(strongly reachable),那么它就不被回收。

  2. 軟引用(SoftReference):軟引用可達的對象在內存不充足時才會被回收,因此軟引用要比弱引用“強”一些。

  3. 弱引用(WeakReference):弱引用簡單來說就是將對象留在內存的能力不是那么強的引用。使用 WeakReference,垃圾回收器決定引用的對象何時回收并且將對象從內存移除。若一個對象是弱引用可達,無論當前內存是否充足它都會被回收

  4. 虛引用(PhantomReference):虛引用是 Java 中最弱的引用,那么它弱到什么程度呢?通過虛引用無法獲取到被引用的對象,虛引用存在的唯一作用就是當它指向的對象被回收后,虛引用本身會被加入到引用隊列中,用作記錄它指向的對象已被回收。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容