本文通過探析Java中的引用模型,分析比較強引用、軟引用、弱引用、虛引用的概念及使用場景,知其然且知其是以然,希望給大家在實際開發實踐、學習開源項目提供參考。
Java的引用
對于Java中的垃圾回收機制來說,對象是否被應該回收的取決于該對象是否被引用。是以,引用也是JVM進行記憶體管理的一個重要概念。Java中是JVM負責記憶體的配置設定和回收,這是它的優點(使用友善,程式不用再像使用C語言那樣擔心記憶體),但同時也是它的缺點(不夠靈活)。由此,Java提供了引用分級模型,可以定義Java對象重要性和優先級,提高JVM記憶體回收的執行效率。
關于引用的定義,在JDK1.2之前,如果reference類型的資料中存儲的數值代表的是另一塊記憶體的起始位址,就稱為這塊記憶體代表着一個引用;JDK1.2之後,Java對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。
軟引用對象和弱應用對象主要用于:當記憶體空間還足夠,則能儲存在記憶體之中;如果記憶體空間在垃圾收集之後還是非常緊張,則可以抛棄這些對象。很多系統的緩存功能都符合這樣的使用場景。
而虛引用對象用于替代不靠譜的finalize方法,可以擷取對象的回收事件,來做資源清理工作。
對象生命周期
無分級引用對象生命周期
前面提到,分層引用的模型是用于記憶體回收,沒有分級引用對象下,一個對象從建立到回收的生命周期可以簡單地用下圖概括:對象被建立,被使用,有資格被收集,最終被收集,陰影區域表示對象“強可達”時間:
對象生命周期(無分級引用)
有分級引用對象生命周期
JDK1.2引入java.lang.ref程式包之後,對象的生命周期多了3個階段,軟可達,弱可達,虛可達,這些狀态僅适用于符合垃圾回收條件的對象,這些對象處于非強引用階段,而且需要基于java.lang.ref包中的相關的引用對象類來訓示标明。
軟可達
軟可達對象用SoftReference來訓示标明,并沒有強引用,垃圾回收器會盡可能長時間地保留對象,但是會在抛出OutOfMemoryError異常之前收集它。
弱可達
弱可達對象用WeakReference來訓示标明,并沒有強引用或軟引用,垃圾回收器會随時回收對象,并不會嘗試保留它,但是會在抛出OutOfMemoryError異常之前收集它。
在對象回收階段中,該對象在major collection期間被回收,但是可以在minor collection期間存活
虛可達
虛可達對象用PhantomReference來訓示标明,它已經被标記選中進行垃圾回收并且它的finalizer(如果有)已經運作。在這種情況下,術語“可達”實際上是用詞不當,因為您無法通路實際對象。
對象生命周期(有分級引用)
對象生命周期圖中添加三個新的可選狀态會造成一些困惑。邏輯順序上是從強可達到軟,弱和虛,最終到回收,但實際的情況取決于程式建立的參考對象。但如果建立WeakReference但不建立SoftReference,則對象直接從強可達到弱到達最終到收集。
強引用
強引用就是指在程式代碼之中普遍存在的,比如下面這段代碼中的obj和str都是強引用:
隻要強引用還存在,垃圾收集器永遠不會回收被引用的對象,即使在記憶體不足的情況下,JVM即使抛出OutOfMemoryError異常也不會回收這種對象。
實際使用上,可以通過把引用顯示指派為null來中斷對象與強引用之前的關聯,如果沒有任何引用執行對象,垃圾收集器将在合适的時間回收對象。
例如ArrayList類的remove方法中就是通過将引用指派為null來實作清理工作的:
引用對象
介紹軟引用、弱引用和虛引用之前,有必要介紹一下引用對象,
引用對象是程式代碼和其他對象之間的間接層,稱為引用對象。每個引用對象都圍繞對象的引用構造,并且不能更改引用值。
引用對象提供get()來獲得其引用值的一個強引用,垃圾收集器可能随時回收引用值所指的對象。
一旦對象被回收,get()方法将傳回null,要正确使用引用對象,下面使用SoftReference(軟引用對象)作為參考示例:
也就是說,使用時:
1、必須經常檢查引用值是否為null
垃圾收集器可能随時回收引用對象,如果輕率地使用引用值,遲早會得到一個NullPointerException。
2、必須使用強引用來指向引用對象傳回的值
垃圾收集器可能在任何時間回收引用對象,即使在一個表達式中間。
3、必須持有引用對象的強引用
如果建立引用對象,沒有持有對象的強引用,那麼引用對象本身将被垃圾收集器回收。
4、當引用值沒有被其他強引用指向時,軟引用、弱引用和虛引用才會發揮作用,引用對象的存在就是為了友善追蹤并高效垃圾回收。
軟引用、弱引用和虛引用
引用對象的3個重要實作類位于java.lang.ref包下,分别是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。
軟引用
軟引用用來描述一些還有用但非必需的對象。對于軟引用關聯着的對象,在系統将要發生抛出OutOfMemoryError異常之前,将會把這些對象列入回收範圍之内進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出OutOfMemoryError異常。在JDK1.2之後,提供了SoftReference類來實作軟引用。
下面是一個使用示例:
JDK文檔中提到:軟引用适用于對記憶體敏感的緩存:每個緩存對象都是通過通路的 SoftReference,如果JVM決定需要記憶體空間,那麼它将清除回收部分或全部軟引用對應的對象。如果它不需要空間,則SoftReference訓示對象保留在堆中,并且可以通過程式代碼通路。在這種情況下,當它們被積極使用時,它們被強引用,否則會被軟引用。如果清除了軟引用,則需要重新整理緩存。
實際使用上,要除非緩存的對象非常大,每個數量級為幾千位元組,才值得考慮使用軟引用對象。例如:實作一個檔案伺服器,它需要定期檢索相同的檔案,或者需要緩存大型對象圖。如果對象很小,必須清除很多對象才能産生影響,那麼不建議使用,因為清除軟引用對象會增加整個過程的開銷。
弱引用
弱引用也是用來描述非必需對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發送之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。
在JDK1.2之後,提供了WeakReference類來實作弱引用。
可以看到被弱引用關聯的對象,在gc之後被回收掉。
有意思的地方是,如果把上面代碼中的:
改為
程式将輸出
這是因為使用Java的String直接指派和使用new差別在于:
new 會在堆區建立一個可以被正常回收的對象。
String直接指派,會在Java StringPool(字元串常量池)裡建立一個String對象,存于pergmen(永生代區)中,通常不會被gc回收。
WeakHashMap
為了更友善使用弱引用,Java還提供了WeakHashMap,功能類似HashMap,内部實作是用弱引用對key進行包裝,當某個key對象沒有任何強引用指向,gc會自動回收key和value對象。
程式輸出:
WeakHashMap比較适用于緩存的場景,例如Tomcat的緩存就用到。
引用隊列
介紹虛引用之前,先介紹引用隊列:
在使用引用對象時,通過判斷get()方法傳回的值是否為null來判斷對象是否已經被回收,當這樣做并不是非常高效,特别是當我們有很多引用對象,如果想找出哪些對象已經被回收,需要周遊所有所有對象。
更好的方案是使用引用隊列,在構造引用對象時與隊列關聯,當gc(垃圾回收線程)準備回收一個對象時,如果發現它還僅有軟引用(或弱引用,或虛引用)指向它,就會在回收該對象之前,把這個軟引用(或弱引用,或虛引用)加入到與之關聯的引用隊列(ReferenceQueue)中。
如果一個軟引用(或弱引用,或虛引用)對象本身在引用隊列中,就說明該引用對象所指向的對象被回收了,是以要找出所有被回收的對象,隻需要周遊引用隊列。
當軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那麼這個引用對象本身就沒有價值了,如果程式中存在大量的這類對象(注意,我們建立的軟引用、弱引用、虛引用對象本身是個強引用,不會自動被gc回收),就會浪費記憶體。是以我們這就可以手動回收位于引用隊列中的引用對象本身。
虛引用
虛引用也稱為幽靈引用或者幻影引用,不同于軟引用和弱引用,虛引用不用于通路引用對象所訓示的對象,相反,通過不斷輪詢虛引用對象關聯的引用隊列,可以得到對象回收事件。一個對象是否有虛引用的存在,完全不會對其生産時間構成影響,也無法通過虛引用來取得一個對象執行個體。雖然這看起來毫無意義,但它實際上可以用來做對象回收時資源清理、釋放,它比finalize更靈活,我們可以基于虛引用做更安全可靠的對象關聯的資源回收。
finalize的問題
Java語言規範并不保證finalize方法會被及時地執行、而且根本不會保證它們會被執行
如果可用記憶體沒有被耗盡,垃圾收集器不會運作,finalize方法也不會被執行。
性能問題
JVM通常在單獨的低優先級線程中完成finalize的執行。
對象再生問題
finalize方法中,可将待回收對象指派給GC Roots可達的對象引用,進而達到對象再生的目的。
針對不靠譜finalize方法,完全可以使用虛引用來實作。在JDK1.2之後,提供了PhantomReference類來實作虛引用。
下面是簡單的使用例子,通過通路引用隊列可以得到對象的回收事件:
比較常見的,可以基于虛引用實作JDBC連接配接池,鎖的釋放等場景。
以連接配接池為例,調用方正常情況下使用完連接配接,需要把連接配接釋放回池中,但是不可避免有可能程式有bug,造成連接配接沒有正常釋放回池中。基于虛引用對Connection對象進行包裝,并關聯引用隊列,就可以通過輪詢引用隊列檢查哪些連接配接對象已經被GC回收,釋放相關連接配接資源。具體實作已上傳github的caison-blog-demo倉庫。
總結
對比一下幾種引用對象的不同:
虛引用,配合引用隊列使用,通過不斷輪詢引用隊列擷取對象回收事件。
雖然引用對象是一個非常有用的工具來管理你的記憶體消耗,但有時它們是不夠的,或者是過度設計的 。例如,使用一個Map來緩存從資料庫中讀取的資料。雖然可以使用弱引用來作為緩存,但最終程式需要運作一定量的記憶體。如果不能給它足夠實際足夠的資源完成任何工作,那麼錯誤恢複機制有多強大也沒有用。
當遇到OutOfMemoryError錯誤,第一反應是要弄清楚它為什麼會發生,也許真的是程式有bug,也許是可用記憶體設定的太低。
在開發過程中,應該制定程式具體的使用記憶體大小,而已要關注實際使用中用了多少記憶體。大多數應用程式在實際運作負載下,程式的記憶體占用會達到穩定狀态,可以用此來作為參考來設定合理的堆大小。如果程式的記憶體使用量随着時間的推移而上升,很有可能是因為當對象不再使用時仍然擁有對對象的強引用。引用對象在這裡可能會有所幫助,但更有可能是把它當做一個bug來進行修複。
歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:860113481
群内提供免費的Java架構學習資料(裡面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!