JVM垃圾收集(ji)策略(lve)與(yu)算法
垃圾收集策略與算法
程(cheng)序計(ji)數器、虛擬機棧(zhan)(zhan)(zhan)(zhan)、本地方(fang)法(fa)棧(zhan)(zhan)(zhan)(zhan)隨(sui)(sui)線程(cheng)而(er)(er)生,也隨(sui)(sui)線程(cheng)而(er)(er)滅;棧(zhan)(zhan)(zhan)(zhan)幀(zhen)隨(sui)(sui)著(zhu)方(fang)法(fa)的(de)開(kai)始而(er)(er)入棧(zhan)(zhan)(zhan)(zhan),隨(sui)(sui)著(zhu)方(fang)法(fa)的(de)結(jie)(jie)束(shu)而(er)(er)出棧(zhan)(zhan)(zhan)(zhan)。這(zhe)幾個區域的(de)內(nei)存(cun)分(fen)配和回收都具(ju)有(you)確定性,在這(zhe)幾個區域內(nei)不需要過多考慮回收的(de)問(wen)題,因為方(fang)法(fa)結(jie)(jie)束(shu)或者線程(cheng)結(jie)(jie)束(shu)時,內(nei)存(cun)自然就跟隨(sui)(sui)著(zhu)回收了。
而(er)對于 Java 堆和方法區,我們只有在程(cheng)序運行期間才能知(zhi)道會創建(jian)哪些對象,這(zhe)部分(fen)(fen)內存(cun)的分(fen)(fen)配和回收都是(shi)動態的,垃(la)圾收集器(qi)所(suo)關注的正(zheng)是(shi)這(zhe)部分(fen)(fen)內存(cun)。
判定對象是否存活
若(ruo)一個(ge)對(dui)象不被(bei)(bei)任何對(dui)象或變量引用,那么它就是無(wu)效(xiao)對(dui)象,需要被(bei)(bei)回收。
一 引用計數法(無法回收循環引用的對象)
在對象(xiang)頭維護(hu)著一個 counter 計(ji)(ji)數(shu)器(qi)(qi),對象(xiang)被引(yin)用一次則計(ji)(ji)數(shu)器(qi)(qi) +1;若(ruo)引(yin)用失效則計(ji)(ji)數(shu)器(qi)(qi) -1。當計(ji)(ji)數(shu)器(qi)(qi)為(wei) 0 時,就(jiu)認(ren)為(wei)該對象(xiang)無效了。
引(yin)用計(ji)數算(suan)法(fa)(fa)的實現簡單(dan),判定效率也很高(gao),在大部分(fen)情況下它都(dou)是(shi)一個不錯的算(suan)法(fa)(fa)。但(dan)是(shi)主流的 Java 虛擬機里(li)沒有選用引(yin)用計(ji)數算(suan)法(fa)(fa)來管(guan)理(li)內(nei)存,主要是(shi)因為它很難解決對(dui)象之間(jian)循環引(yin)用的問題。
例子:對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它(ta)們互相引用(yong)著對方,導致(zhi)它(ta)們的引用(yong)計數都不為(wei) 0,于是引用(yong)計數算法無法通(tong)知 GC 收集器回收它(ta)們。
二 可達性分析法(更好)
所有和 GC Roots 直接(jie)或間接(jie)關(guan)聯的對(dui)象都是(shi)(shi)有效對(dui)象,和 GC Roots 沒有關(guan)聯的對(dui)象就是(shi)(shi)無(wu)效對(dui)象。
GC Roots 是指(zhi):
- Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 本地方法棧中引用的對象
- 方法區中常量引用的對象
- 方法區中類靜態屬性引用的對象
GC Roots 并不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。
引用的種類
判定對(dui)象(xiang)是否存活(huo)與“引用”有(you)關。在 JDK 1.2 以(yi)前,Java 中(zhong)的引用定義(yi)很傳統(tong),一個對(dui)象(xiang)只有(you)被引用或者沒(mei)有(you)被引用兩種(zhong)狀態,我們希望能描(miao)述這一類對(dui)象(xiang):當(dang)內(nei)存空間(jian)還足夠時,則保(bao)留在內(nei)存中(zhong);如(ru)果內(nei)存空間(jian)在進行(xing)垃圾手收集后還是非常緊張,則可以(yi)拋(pao)棄(qi)這些(xie)對(dui)象(xiang)。很多系(xi)統(tong)的緩存功能都符合這樣的應用場景。
在 JDK 1.2 之后(hou),Java 對(dui)引用的(de)概念進行了擴(kuo)充,將引用分為了以下(xia)四種。不同的(de)引用類型,主(zhu)要體(ti)現的(de)是對(dui)象(xiang)不同的(de)可(ke)達性狀態reachable和垃圾收集的(de)影(ying)響。
強引用(Strong Reference)
類似 "Object obj = new Object()" 這類的引(yin)用,就是強引(yin)用,只要強引(yin)用存(cun)(cun)在(zai),垃圾(ji)收集器永遠(yuan)不(bu)會(hui)(hui)回(hui)收被引(yin)用的對象。但是,如果(guo)我們錯誤地保持了(le)強引(yin)用,比如:賦值給了(le) static 變(bian)量,那么對象在(zai)很長(chang)一段時間(jian)內不(bu)會(hui)(hui)被回(hui)收,會(hui)(hui)產生內存(cun)(cun)泄漏。
軟引用(Soft Reference)
軟(ruan)(ruan)引用是一種相對強引用弱(ruo)化一些的(de)引用,可以讓對象(xiang)豁免一些垃圾收(shou)(shou)集,只有當(dang) JVM 認為內存(cun)(cun)不足時,才會(hui)去試圖回收(shou)(shou)軟(ruan)(ruan)引用指(zhi)向的(de)對象(xiang)。JVM 會(hui)確(que)保在拋出 OutOfMemoryError 之前,清(qing)(qing)理(li)軟(ruan)(ruan)引用指(zhi)向的(de)對象(xiang)。軟(ruan)(ruan)引用通常用來實現內存(cun)(cun)敏感的(de)緩(huan)存(cun)(cun),如(ru)果(guo)還有空閑內存(cun)(cun),就(jiu)可以暫(zan)時保留緩(huan)存(cun)(cun),當(dang)內存(cun)(cun)不足時清(qing)(qing)理(li)掉,這樣就(jiu)保證(zheng)了(le)使用緩(huan)存(cun)(cun)的(de)同時,不會(hui)耗盡內存(cun)(cun)。
弱引用(Weak Reference)
弱(ruo)(ruo)引(yin)(yin)(yin)用(yong)的強度比軟引(yin)(yin)(yin)用(yong)更弱(ruo)(ruo)一(yi)些。當 JVM 進行垃圾回收(shou)(shou)時(shi),無(wu)論(lun)內存是否充足,都會回收(shou)(shou)只被弱(ruo)(ruo)引(yin)(yin)(yin)用(yong)關聯的對象。
虛引用(Phantom Reference)
虛引(yin)用也(ye)稱幽靈引(yin)用或者幻(huan)影(ying)引(yin)用,它(ta)是最(zui)弱的(de)一(yi)種引(yin)用關系。一(yi)個對象是否有(you)虛引(yin)用的(de)存(cun)(cun)在(zai),完全(quan)不會(hui)對其生存(cun)(cun)時間構成影(ying)響(xiang)。它(ta)僅僅是提供了一(yi)種確保(bao)對象被 finalize 以后,做某些事(shi)情的(de)機制(zhi),比如,通常用來做所謂的(de) Post-Mortem 清理機制(zhi)。
回收堆中無效對象
對于可(ke)達性分析(xi)中不可(ke)達的對象,也并不是沒有存(cun)活(huo)的可(ke)能(neng)。
判定 finalize() 是否有必要執行
JVM 會判(pan)斷此對象是否有(you)(you)必要執(zhi)行 finalize() 方(fang)(fang)法,如果對象沒(mei)有(you)(you)覆蓋 finalize() 方(fang)(fang)法,或者 finalize() 方(fang)(fang)法已經被(bei)虛擬機調(diao)用過,那么視為“沒(mei)有(you)(you)必要執(zhi)行”。那么對象基(ji)本上就真的被(bei)回收了(le)。
如(ru)果對象(xiang)被判定為有(you)必要執(zhi)行 finalize() 方(fang)法(fa)(fa),那么對象(xiang)會被放入(ru)一個 F-Queue 隊列中,虛擬(ni)機會以較低的優先級執(zhi)行這些 finalize()方(fang)法(fa)(fa),但不會確保所有(you)的 finalize() 方(fang)法(fa)(fa)都會執(zhi)行結束(shu)。如(ru)果 finalize() 方(fang)法(fa)(fa)出現(xian)耗時(shi)操作,虛擬(ni)機就直接停止(zhi)指向該方(fang)法(fa)(fa),將對象(xiang)清除。
對象重生或死亡
如果在執行(xing) finalize() 方法時,將 this 賦(fu)給(gei)了(le)某一個引用,那么該對象就(jiu)重生了(le)。如果沒有,那么就(jiu)會被垃圾收集器清除。
任何一(yi)個對象的 finalize() 方法(fa)只(zhi)會被(bei)系(xi)統(tong)自動調用(yong)一(yi)次(ci),如果對象面臨(lin)下一(yi)次(ci)回收,它的 finalize() 方法(fa)不會被(bei)再次(ci)執行,想繼續(xu)在 finalize() 中自救就(jiu)失效(xiao)了。
回收方法區內存
方法區中存放生命周期較長(chang)的(de)類信息、常量、靜態變量,每次垃圾收集只有少量的(de)垃圾被(bei)清除(chu)。方法區中主要(yao)清除(chu)兩種(zhong)垃圾:
- 廢棄常量
- 無用的類
判定廢棄常量
只(zhi)要常(chang)(chang)量(liang)(liang)池中的常(chang)(chang)量(liang)(liang)不(bu)被(bei)任何(he)變量(liang)(liang)或對象(xiang)引(yin)用,那么這(zhe)些常(chang)(chang)量(liang)(liang)就會被(bei)清除(chu)掉。比如,一(yi)個字符串 "bingo" 進入了常(chang)(chang)量(liang)(liang)池,但是當前(qian)系統沒有任何(he)一(yi)個 String 對象(xiang)引(yin)用常(chang)(chang)量(liang)(liang)池中的 "bingo" 常(chang)(chang)量(liang)(liang),也(ye)沒有其它(ta)地方引(yin)用這(zhe)個字面量(liang)(liang),必要的話,"bingo"常(chang)(chang)量(liang)(liang)會被(bei)清理出(chu)常(chang)(chang)量(liang)(liang)池。
判定無用的類
判定一個類是否是“無用的(de)類”,條件(jian)較為苛刻(ke)。
- 該類的所有對象都已經被清除
- 加載該類的 ClassLoader 已經被回收
- 該類的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
一個(ge)類(lei)被(bei)虛(xu)擬(ni)機加載(zai)進方(fang)(fang)法(fa)區,那么在(zai)堆中就會有一個(ge)代(dai)表該類(lei)的(de)對(dui)象:java.lang.Class。這個(ge)對(dui)象在(zai)類(lei)被(bei)加載(zai)進方(fang)(fang)法(fa)區時創建,在(zai)方(fang)(fang)法(fa)區該類(lei)被(bei)刪除(chu)(chu)時清(qing)除(chu)(chu)。
垃圾收集算法
學會(hui)了如何判定無(wu)(wu)效(xiao)對象、無(wu)(wu)用(yong)類、廢棄常(chang)量之后,剩余(yu)工作就是回收這些垃圾(ji)。常(chang)見的垃圾(ji)收集算法有以下幾(ji)個(ge):
標記-清除算法
標記的過程是:遍歷所有的(de)(de) GC Roots,然后將所有 GC Roots 可達的(de)(de)對象標記為存活的(de)(de)對象。
清除的過程:將遍歷堆中所(suo)有的(de)(de)對象,將沒有標(biao)記的(de)(de)對象全部清除掉。與此同時,清除那些被標(biao)記過的(de)(de)對象的(de)(de)標(biao)記,以便下次的(de)(de)垃圾(ji)回收(shou)。
這種方法有兩個(ge)不足(zu):
- 效率問題:標記和清除兩個過程的效率都不高。
- 空間問題:標記清除之后會產生大量不連續的內存碎片,碎片太多可能導致以后需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
復制算法(新生代)
為(wei)了(le)解決效率問(wen)題(ti),“復制(zhi)”收集算(suan)(suan)法出現了(le)。它將可用內存(cun)按容量劃(hua)分(fen)為(wei)大小相等的兩塊(kuai),每(mei)次只使用其中的一(yi)(yi)塊(kuai)。當這一(yi)(yi)塊(kuai)內存(cun)用完,需要(yao)進(jin)行垃圾收集時,就將存(cun)活者的對象(xiang)復制(zhi)到另一(yi)(yi)塊(kuai)上(shang)面,然(ran)后(hou)將第一(yi)(yi)塊(kuai)內存(cun)全(quan)部清除。這種算(suan)(suan)法有優有劣(lie):
- 優點:不會有內存碎片的問題。
- 缺點:內存縮小為原來的一半,浪費空間。
為了解決空間利用率問題,可以將內存分為三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才使用的 Survivor 空間。這樣只有 10% 的內存被浪費。
但是我們無法保(bao)證每次回收都(dou)只(zhi)有不多于 10% 的對象存活(huo),當 Survivor 空(kong)間(jian)不夠,需要依賴(lai)其他內存(指老年代)進行分配擔保(bao)。
分配擔保
為對(dui)象分(fen)配(pei)內(nei)存空(kong)間(jian)時,如果 Eden+Survivor 中空(kong)閑區域無法裝下(xia)該對(dui)象,會(hui)觸(chu)發 MinorGC 進(jin)行(xing)垃圾收集。但(dan)如果 Minor GC 過后依然有(you)超過 10% 的對(dui)象存活,這(zhe)樣存活的對(dui)象直接通過分(fen)配(pei)擔(dan)保機(ji)制(zhi)進(jin)入老(lao)年(nian)代,然后再將新對(dui)象存入 Eden 區。
標記-整理算法(老年代)
標記:它的第(di)一個(ge)階(jie)段與(yu)標(biao)記(ji)(ji)/清除算法是(shi)一模一樣(yang)的,均是(shi)遍(bian)歷 GC Roots,然后(hou)將(jiang)存(cun)活(huo)的對象標(biao)記(ji)(ji)。
整理:移(yi)動所(suo)有存(cun)活(huo)的(de)(de)對象,且(qie)按照內(nei)存(cun)地址次(ci)序依次(ci)排列(lie),然后將末(mo)端(duan)內(nei)存(cun)地址以后的(de)(de)內(nei)存(cun)全(quan)部回收。因(yin)此,第二階段才稱(cheng)為整理階段。
這是一(yi)種老(lao)年代的(de)垃圾(ji)(ji)收集算法(fa)(fa)。老(lao)年代的(de)對(dui)象一(yi)般壽命(ming)比較長,因(yin)此每(mei)次垃圾(ji)(ji)回收會(hui)有(you)大量對(dui)象存(cun)活,如(ru)果采用復制算法(fa)(fa),每(mei)次需要復制大量存(cun)活的(de)對(dui)象,效率很低。
分代收集算法
根(gen)據對象存(cun)活周(zhou)期的不同,將內存(cun)劃(hua)分(fen)為幾塊。一般是把 Java 堆分(fen)為新(xin)生代和老年代,針對各(ge)個年代的特點采用最適當(dang)的收集算(suan)法。
新生代:復制算法
老年代:標(biao)記-清除算法、標(biao)記-整(zheng)理算法