彻底理解JVM常考题之分级引用模型
轉(zhuǎn)載自? ?徹底理解JVM??碱}之分級引用模型
本文通過探析Java中的引用模型,分析比較強(qiáng)引用、軟引用、弱引用、虛引用的概念及使用場景,知其然且知其所以然,希望給大家在實(shí)際開發(fā)實(shí)踐、學(xué)習(xí)開源項(xiàng)目提供參考。
Java的引用
對于Java中的垃圾回收機(jī)制來說,對象是否被應(yīng)該回收的取決于該對象是否被引用。因此,引用也是JVM進(jìn)行內(nèi)存管理的一個(gè)重要概念。Java中是JVM負(fù)責(zé)內(nèi)存的分配和回收,這是它的優(yōu)點(diǎn)(使用方便,程序不用再像使用C語言那樣擔(dān)心內(nèi)存),但同時(shí)也是它的缺點(diǎn)(不夠靈活)。由此,Java提供了引用分級模型,可以定義Java對象重要性和優(yōu)先級,提高JVM內(nèi)存回收的執(zhí)行效率。
關(guān)于引用的定義,在JDK1.2之前,如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另一塊內(nèi)存的起始地址,就稱為這塊內(nèi)存代表著一個(gè)引用;JDK1.2之后,Java對引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。
軟引用對象和弱應(yīng)用對象主要用于:當(dāng)內(nèi)存空間還足夠,則能保存在內(nèi)存之中;如果內(nèi)存空間在垃圾收集之后還是非常緊張,則可以拋棄這些對象。很多系統(tǒng)的緩存功能都符合這樣的使用場景。
而虛引用對象用于替代不靠譜的finalize方法,可以獲取對象的回收事件,來做資源清理工作。
?
對象生命周期
無分級引用對象生命周期
前面提到,分層引用的模型是用于內(nèi)存回收,沒有分級引用對象下,一個(gè)對象從創(chuàng)建到回收的生命周期可以簡單地用下圖概括:對象被創(chuàng)建,被使用,有資格被收集,最終被收集,陰影區(qū)域表示對象“強(qiáng)可達(dá)”時(shí)間:
有分級引用對象生命周期
JDK1.2引入java.lang.ref程序包之后,對象的生命周期多了3個(gè)階段,軟可達(dá),弱可達(dá),虛可達(dá),這些狀態(tài)僅適用于符合垃圾回收條件的對象,這些對象處于非強(qiáng)引用階段,而且需要基于java.lang.ref包中的相關(guān)的引用對象類來指示標(biāo)明。
-
軟可達(dá)
軟可達(dá)對象用SoftReference來指示標(biāo)明,并沒有強(qiáng)引用,垃圾回收器會(huì)盡可能長時(shí)間地保留對象,但是會(huì)在拋出OutOfMemoryError異常之前收集它。 -
弱可達(dá)
弱可達(dá)對象用WeakReference來指示標(biāo)明,并沒有強(qiáng)引用或軟引用,垃圾回收器會(huì)隨時(shí)回收對象,并不會(huì)嘗試保留它,但是會(huì)在拋出OutOfMemoryError異常之前收集它。
在對象回收階段中,該對象在major collection期間被回收,但是可以在minor collection期間存活 -
虛可達(dá)?
虛可達(dá)對象用PhantomReference來指示標(biāo)明,它已經(jīng)被標(biāo)記選中進(jìn)行垃圾回收并且它的finalizer(如果有)已經(jīng)運(yùn)行。在這種情況下,術(shù)語“可達(dá)”實(shí)際上是用詞不當(dāng),因?yàn)槟鸁o法訪問實(shí)際對象。
對象生命周期圖中添加三個(gè)新的可選狀態(tài)會(huì)造成一些困惑。邏輯順序上是從強(qiáng)可達(dá)到軟,弱和虛,最終到回收,但實(shí)際的情況取決于程序創(chuàng)建的參考對象。但如果創(chuàng)建WeakReference但不創(chuàng)建SoftReference,則對象直接從強(qiáng)可達(dá)到弱到達(dá)最終到收集。
?
強(qiáng)引用
強(qiáng)引用就是指在程序代碼之中普遍存在的,比如下面這段代碼中的obj和str都是強(qiáng)引用:
Object?obj?=?new?Object(); String?str?=?"hello?world";只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收被引用的對象,即使在內(nèi)存不足的情況下,JVM即使拋出OutOfMemoryError異常也不會(huì)回收這種對象。
實(shí)際使用上,可以通過把引用顯示賦值為null來中斷對象與強(qiáng)引用之前的關(guān)聯(lián),如果沒有任何引用執(zhí)行對象,垃圾收集器將在合適的時(shí)間回收對象。
例如ArrayList類的remove方法中就是通過將引用賦值為null來實(shí)現(xiàn)清理工作的:
????/***?Removes?the?element?at?the?specified?position?in?this?list.*?Shifts?any?subsequent?elements?to?the?left?(subtracts?one?from?their*?indices).**?@param?index?the?index?of?the?element?to?be?removed*?@return?the?element?that?was?removed?from?the?list*?@throws?IndexOutOfBoundsException?{@inheritDoc}*/public?E?remove(int?index)?{rangeCheck(index);modCount++;E?oldValue?=?elementData(index);int?numMoved?=?size?-?index?-?1;if?(numMoved?>?0)System.arraycopy(elementData,?index+1,?elementData,?index,numMoved);elementData[--size]?=?null;?//?clear?to?let?GC?do?its?workreturn?oldValue;}?
引用對象
介紹軟引用、弱引用和虛引用之前,有必要介紹一下引用對象,
引用對象是程序代碼和其他對象之間的間接層,稱為引用對象。每個(gè)引用對象都圍繞對象的引用構(gòu)造,并且不能更改引用值。
引用對象提供get()來獲得其引用值的一個(gè)強(qiáng)引用,垃圾收集器可能隨時(shí)回收引用值所指的對象。
一旦對象被回收,get()方法將返回null,要正確使用引用對象,下面使用SoftReference(軟引用對象)作為參考示例:
也就是說,使用時(shí):
-
1、必須經(jīng)常檢查引用值是否為null
垃圾收集器可能隨時(shí)回收引用對象,如果輕率地使用引用值,遲早會(huì)得到一個(gè)NullPointerException。 -
2、必須使用強(qiáng)引用來指向引用對象返回的值
垃圾收集器可能在任何時(shí)間回收引用對象,即使在一個(gè)表達(dá)式中間。
-
3、必須持有引用對象的強(qiáng)引用
如果創(chuàng)建引用對象,沒有持有對象的強(qiáng)引用,那么引用對象本身將被垃圾收集器回收。 -
4、當(dāng)引用值沒有被其他強(qiáng)引用指向時(shí),軟引用、弱引用和虛引用才會(huì)發(fā)揮作用,引用對象的存在就是為了方便追蹤并高效垃圾回收。
?
軟引用、弱引用和虛引用
引用對象的3個(gè)重要實(shí)現(xiàn)類位于java.lang.ref包下,分別是軟引用SoftReference、弱引用WeakReference和虛引用PhantomReference。
軟引用
軟引用用來描述一些還有用但非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生拋出OutOfMemoryError異常之前,將會(huì)把這些對象列入回收范圍之內(nèi)進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會(huì)拋出OutOfMemoryError異常。在JDK1.2之后,提供了SoftReference類來實(shí)現(xiàn)軟引用。
下面是一個(gè)使用示例:
import?java.lang.ref.SoftReference;public?class?SoftRefDemo?{public?static?void?main(String[]?args)?{SoftReference<String>?sr?=?new?SoftReference<>(?new?String("hello?world?"));//?hello?worldSystem.out.println(sr.get());} }JDK文檔中提到:軟引用適用于對內(nèi)存敏感的緩存:每個(gè)緩存對象都是通過訪問的 SoftReference,如果JVM決定需要內(nèi)存空間,那么它將清除回收部分或全部軟引用對應(yīng)的對象。如果它不需要空間,則SoftReference指示對象保留在堆中,并且可以通過程序代碼訪問。在這種情況下,當(dāng)它們被積極使用時(shí),它們被強(qiáng)引用,否則會(huì)被軟引用。如果清除了軟引用,則需要刷新緩存。
實(shí)際使用上,要除非緩存的對象非常大,每個(gè)數(shù)量級為幾千字節(jié),才值得考慮使用軟引用對象。例如:實(shí)現(xiàn)一個(gè)文件服務(wù)器,它需要定期檢索相同的文件,或者需要緩存大型對象圖。如果對象很小,必須清除很多對象才能產(chǎn)生影響,那么不建議使用,因?yàn)榍宄浺脤ο髸?huì)增加整個(gè)過程的開銷。
弱引用
弱引用也是用來描述非必需對象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)送之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對象。
在JDK1.2之后,提供了WeakReference類來實(shí)現(xiàn)弱引用。
????/***?簡單使用弱引用demo*/private?static?void?simpleUseWeakRefDemo(){WeakReference<String>?sr?=?new?WeakReference<>(new?String("hello?world?"?));//?before?gc?->?hello?world?System.out.println("before?gc?->?"?+?sr.get());//?通知JVM的gc進(jìn)行垃圾回收System.gc();//?after?gc?->?nullSystem.out.println("after?gc?->?"?+?sr.get());}可以看到被弱引用關(guān)聯(lián)的對象,在gc之后被回收掉。
有意思的地方是,如果把上面代碼中的:
改為
WeakReference<String>?sr?=?new?WeakReference<>("hello?world?");程序?qū)⑤敵?/p> before?gc?->?hello?world? after?gc?->?hello?world?
這是因?yàn)槭褂肑ava的String直接賦值和使用new區(qū)別在于:
-
new 會(huì)在堆區(qū)創(chuàng)建一個(gè)可以被正?;厥盏膶ο蟆?/p>
-
String直接賦值,會(huì)在Java StringPool(字符串常量池)里創(chuàng)建一個(gè)String對象,存于pergmen(永生代區(qū))中,通常不會(huì)被gc回收。
WeakHashMap
為了更方便使用弱引用,Java還提供了WeakHashMap,功能類似HashMap,內(nèi)部實(shí)現(xiàn)是用弱引用對key進(jìn)行包裝,當(dāng)某個(gè)key對象沒有任何強(qiáng)引用指向,gc會(huì)自動(dòng)回收key和value對象。
程序輸出:
before:?gc?weakHashMap?=?{key1=value1,?key2=value2,?key3=value3}?,?size=3 after:?gc?weakHashMap?=?{key2=value2,?key3=value3}?,?size=2WeakHashMap比較適用于緩存的場景,例如Tomcat的緩存就用到。
引用隊(duì)列
介紹虛引用之前,先介紹引用隊(duì)列:
在使用引用對象時(shí),通過判斷get()方法返回的值是否為null來判斷對象是否已經(jīng)被回收,當(dāng)這樣做并不是非常高效,特別是當(dāng)我們有很多引用對象,如果想找出哪些對象已經(jīng)被回收,需要遍歷所有所有對象。
更好的方案是使用引用隊(duì)列,在構(gòu)造引用對象時(shí)與隊(duì)列關(guān)聯(lián),當(dāng)gc(垃圾回收線程)準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它還僅有軟引用(或弱引用,或虛引用)指向它,就會(huì)在回收該對象之前,把這個(gè)軟引用(或弱引用,或虛引用)加入到與之關(guān)聯(lián)的引用隊(duì)列(ReferenceQueue)中。
如果一個(gè)軟引用(或弱引用,或虛引用)對象本身在引用隊(duì)列中,就說明該引用對象所指向的對象被回收了,所以要找出所有被回收的對象,只需要遍歷引用隊(duì)列。
當(dāng)軟引用(或弱引用,或虛引用)對象所指向的對象被回收了,那么這個(gè)引用對象本身就沒有價(jià)值了,如果程序中存在大量的這類對象(注意,我們創(chuàng)建的軟引用、弱引用、虛引用對象本身是個(gè)強(qiáng)引用,不會(huì)自動(dòng)被gc回收),就會(huì)浪費(fèi)內(nèi)存。因此我們這就可以手動(dòng)回收位于引用隊(duì)列中的引用對象本身。
????/***?引用隊(duì)列demo*/private?static?void?refQueueDemo()?{ReferenceQueue<String>?refQueue?=?new?ReferenceQueue<>();//?用于檢查引用隊(duì)列中的引用值被回收Thread?checkRefQueueThread?=?new?Thread(()?->?{while?(true)?{Reference<??extends?String>?clearRef?=?refQueue.poll();if?(null?!=?clearRef)?{System.out.println("引用對象被回收,?ref?=?"?+?clearRef?+?",?value?=?"?+?clearRef.get());}}});checkRefQueueThread.start();WeakReference<String>?weakRef1?=?new?WeakReference<>(new?String("value1"),?refQueue);WeakReference<String>?weakRef2?=?new?WeakReference<>(new?String("value2"),?refQueue);WeakReference<String>?weakRef3?=?new?WeakReference<>(new?String("value3"),?refQueue);System.out.println("ref1?value?=?"?+?weakRef1.get()?+?",?ref2?value?=?"?+?weakRef2.get()+?",?ref3?value?=?"?+?weakRef3.get());System.out.println("開始通知JVM的gc進(jìn)行垃圾回收");//?通知JVM的gc進(jìn)行垃圾回收System.gc();}程序輸出:
ref1?value?=?value1,?ref2?value?=?value2,?ref3?value?=?value3 開始通知JVM的gc進(jìn)行垃圾回收 引用對象被回收,?ref?=?java.lang.ref.WeakReference@48c6cd96,?value=null 引用對象被回收,?ref?=?java.lang.ref.WeakReference@46013afe,?value=null 引用對象被回收,?ref?=?java.lang.ref.WeakReference@423ea6e6,?value=null虛引用
虛引用也稱為幽靈引用或者幻影引用,不同于軟引用和弱引用,虛引用不用于訪問引用對象所指示的對象,相反,通過不斷輪詢虛引用對象關(guān)聯(lián)的引用隊(duì)列,可以得到對象回收事件。一個(gè)對象是否有虛引用的存在,完全不會(huì)對其生產(chǎn)時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對象實(shí)例。雖然這看起來毫無意義,但它實(shí)際上可以用來做對象回收時(shí)資源清理、釋放,它比finalize更靈活,我們可以基于虛引用做更安全可靠的對象關(guān)聯(lián)的資源回收。
-
finalize的問題
-
Java語言規(guī)范并不保證finalize方法會(huì)被及時(shí)地執(zhí)行、而且根本不會(huì)保證它們會(huì)被執(zhí)行
如果可用內(nèi)存沒有被耗盡,垃圾收集器不會(huì)運(yùn)行,finalize方法也不會(huì)被執(zhí)行。 -
性能問題
JVM通常在單獨(dú)的低優(yōu)先級線程中完成finalize的執(zhí)行。 -
對象再生問題
finalize方法中,可將待回收對象賦值給GC Roots可達(dá)的對象引用,從而達(dá)到對象再生的目的。
針對不靠譜finalize方法,完全可以使用虛引用來實(shí)現(xiàn)。在JDK1.2之后,提供了PhantomReference類來實(shí)現(xiàn)虛引用。
下面是簡單的使用例子,通過訪問引用隊(duì)列可以得到對象的回收事件:
????/***?簡單使用虛引用demo*?虛引用在實(shí)現(xiàn)一個(gè)對象被回收之前必須做清理操作是很有用的,比finalize()方法更靈活*/private?static?void?simpleUsePhantomRefDemo()?throws?InterruptedException?{Object?obj?=?new?Object();ReferenceQueue<Object>?refQueue?=?new?ReferenceQueue<>();PhantomReference<Object>?phantomRef?=?new?PhantomReference<>(obj,?refQueue);//?nullSystem.out.println(phantomRef.get());//?nullSystem.out.println(refQueue.poll());obj?=?null;//?通知JVM的gc進(jìn)行垃圾回收System.gc();//?null,?調(diào)用phantomRef.get()不管在什么情況下會(huì)一直返回nullSystem.out.println(phantomRef.get());//?當(dāng)GC發(fā)現(xiàn)了虛引用,GC會(huì)將phantomRef插入進(jìn)我們之前創(chuàng)建時(shí)傳入的refQueue隊(duì)列//?注意,此時(shí)phantomRef對象,并沒有被GC回收,在我們顯式地調(diào)用refQueue.poll返回phantomRef之后//?當(dāng)GC第二次發(fā)現(xiàn)虛引用,而此時(shí)JVM將phantomRef插入到refQueue會(huì)插入失敗,此時(shí)GC才會(huì)對phantomRef對象進(jìn)行回收Thread.sleep(200);Reference<?>?pollObj?=?refQueue.poll();//?java.lang.ref.PhantomReference@1540e19dSystem.out.println(pollObj);if?(null?!=?pollObj)?{//?進(jìn)行資源回收的操作}}比較常見的,可以基于虛引用實(shí)現(xiàn)JDBC連接池,鎖的釋放等場景。
以連接池為例,調(diào)用方正常情況下使用完連接,需要把連接釋放回池中,但是不可避免有可能程序有bug,造成連接沒有正常釋放回池中。基于虛引用對Connection對象進(jìn)行包裝,并關(guān)聯(lián)引用隊(duì)列,就可以通過輪詢引用隊(duì)列檢查哪些連接對象已經(jīng)被GC回收,釋放相關(guān)連接資源。具體實(shí)現(xiàn)已上傳github的caison-blog-demo倉庫。
?
總結(jié)
對比一下幾種引用對象的不同:
| 強(qiáng)引用 | 永不 | 對象的一般狀態(tài) | JVM停止運(yùn)行時(shí) |
| 軟引用 | 內(nèi)存不足時(shí) | 對象緩存 | 內(nèi)存不足時(shí)終止 |
| 弱引用 | GC時(shí) | 對象緩存 | GC后終止 |
虛引用,配合引用隊(duì)列使用,通過不斷輪詢引用隊(duì)列獲取對象回收事件。
雖然引用對象是一個(gè)非常有用的工具來管理你的內(nèi)存消耗,但有時(shí)它們是不夠的,或者是過度設(shè)計(jì)的 。例如,使用一個(gè)Map來緩存從數(shù)據(jù)庫中讀取的數(shù)據(jù)。雖然可以使用弱引用來作為緩存,但最終程序需要運(yùn)行一定量的內(nèi)存。如果不能給它足夠?qū)嶋H足夠的資源完成任何工作,那么錯(cuò)誤恢復(fù)機(jī)制有多強(qiáng)大也沒有用。
當(dāng)遇到OutOfMemoryError錯(cuò)誤,第一反應(yīng)是要弄清楚它為什么會(huì)發(fā)生,也許真的是程序有bug,也許是可用內(nèi)存設(shè)置的太低。
在開發(fā)過程中,應(yīng)該制定程序具體的使用內(nèi)存大小,而已要關(guān)注實(shí)際使用中用了多少內(nèi)存。大多數(shù)應(yīng)用程序在實(shí)際運(yùn)行負(fù)載下,程序的內(nèi)存占用會(huì)達(dá)到穩(wěn)定狀態(tài),可以用此來作為參考來設(shè)置合理的堆大小。如果程序的內(nèi)存使用量隨著時(shí)間的推移而上升,很有可能是因?yàn)楫?dāng)對象不再使用時(shí)仍然擁有對對象的強(qiáng)引用。引用對象在這里可能會(huì)有所幫助,但更有可能是把它當(dāng)做一個(gè)bug來進(jìn)行修復(fù)。
文章所有涉及源碼已經(jīng)上傳github,地址:https://github.com/caison/caison-blog-demo,可以點(diǎn)擊查看原文獲取。
總結(jié)
以上是生活随笔為你收集整理的彻底理解JVM常考题之分级引用模型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Duda vs Wix Site Bui
- 下一篇: 一个简单的例子,学习自定义注解和AOP