JVM05-垃圾收集算法

前言

上一篇我們介紹了JVM04-JVM中內(nèi)存溢出以及其處理方法。這一篇文章我們來熟悉下JVM中各種垃圾回收算法。這些垃圾收集算法是后面各種垃圾收集器的算法基礎(chǔ)。閑話少敘,讓我們直入主題。
標(biāo)記-清除算法

標(biāo)記-清除算法分為"標(biāo)記"和"清除"兩個階段,首先標(biāo)記所有需要回收的對象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對象,也可以反過來,標(biāo)記存活的對象,統(tǒng)一回收未標(biāo)記的對象,標(biāo)記的過程就是對象是否屬于垃圾的判定過程,一般是通過可達(dá)性分析算法,也就是說某個對象到GC Root間是否有引用鏈相連,如果沒有則判斷該對象不再被使用,也就是說是可以被回收的。如下圖所示,標(biāo)黃的內(nèi)存塊,在標(biāo)記之后,就被清除了,留下來不少的內(nèi)存碎片。
在這里插入圖片描述

標(biāo)記-清除算法的優(yōu)點

實現(xiàn)簡單
標(biāo)記-清除算法實現(xiàn)簡單, 與其他算法的組合也相應(yīng)地簡單。
與保守式GC算法兼容中,對象是不能被移動的,因此保守式GC算法跟把對象從現(xiàn)在的場所復(fù)制算法與標(biāo)記-壓縮算法不兼容。標(biāo)記-清除算法因為不會移動對象,所以非常適合搭配保守式GC算法。事實上,在很多采用保守式GC算法的處理程序中也用到了標(biāo)記-清除算法。

標(biāo)記-清除算法的缺點

分配速度
執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對象,而且這些對象大部分是需要回收的,這是必須進(jìn)行大量標(biāo)記和清除動作,導(dǎo)致標(biāo)記和清除兩個過程的執(zhí)行效率都隨著對象數(shù)量增長而降低。
碎片化
內(nèi)存空間的碎片問題、標(biāo)記、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多會導(dǎo)致當(dāng)以后在程序運行過程中需要分配較大對象時無法找到足夠連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

標(biāo)記-復(fù)制算法

標(biāo)記-復(fù)制算法將可用的內(nèi)存容量劃分為大小相等的兩塊,每次只使用其中的一塊,當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對象都是存活的,這種算法將會產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對于多數(shù)對象都是可回收的情況下,算法需要復(fù)制的就是占少數(shù)的存活對象。而且每次都是針對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時就不用考慮有內(nèi)存碎片的復(fù)雜情況了。如下圖所示:回收之后將存活的對象全部移動到原來的保留區(qū)域。
在這里插入圖片描述

標(biāo)記-復(fù)制算法的優(yōu)點

優(yōu)秀的吞吐量
標(biāo)記-清除算法消耗的吞吐量是搜索活動對象(標(biāo)記階段)所花費的時間和搜索整體堆(清除階段)所花費的時間之和。
另一方面,因為標(biāo)記-復(fù)制算法只搜索并復(fù)制活動對象,所以跟一般的標(biāo)記-清除算法相比,它能在較短時間內(nèi)完成GC,也就是說,其吞吐量優(yōu)秀。
尤其是堆越大,差距越明顯。
可實現(xiàn)高速分配
標(biāo)記-復(fù)制算法不使用空閑鏈表,這是因為分塊是一塊連續(xù)的內(nèi)存空間,比起標(biāo)記-清除算法等使用空閑鏈表的分配,標(biāo)記-復(fù)制算法明顯快得多。
不會發(fā)生碎片化
存活對象被幾種安排到保留區(qū)域,像這樣把對象重新集中,放在堆的一端的行為就叫作壓縮,在標(biāo)記-復(fù)制算法中,每次運行GC時都會執(zhí)行壓縮。因此復(fù)制算法不會發(fā)生碎片化。

標(biāo)記-復(fù)制算法的缺點

堆使用效率低下
標(biāo)記-復(fù)制算法把堆二等分,通常只能利用其中的一半來安排對象,也就是說,只有一半的堆能被使用,相比其他能使用整個堆的GC算法而言,這是標(biāo)記-復(fù)制算法的一個重大缺陷。
不兼容保守式GC算法
標(biāo)記-復(fù)制算法因為必須要移動對象重寫指針,所以跟保守式GC算法不相容。

Appel式回收

在1989年,Andrew Appel針對具備"朝生夕滅"特點的對象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略,稱之為"Appel式回收"。Appel式回收的具體做法就是把新生代分為一塊較大的Eden和Survivor中仍然存活的對象一次性復(fù)制到另一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間,HotSpot虛擬機(jī) 默認(rèn)Eden和Survivor的大小比例是8:1,也即每次新生代可用空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代會被"浪費"掉。
標(biāo)記-整理算法

標(biāo)記-復(fù)制算法在對象存活率較高時就要進(jìn)行較多的復(fù)制操作,效率將會降低。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況。所以老年代一般不能直接選用這種算法。所以,針對老年代的垃圾收集,有標(biāo)記-整理算法,首先還是標(biāo)記所有需要回收的對象,然后讓所有存活的對象都向內(nèi)存一端移動,然后,直接清理掉邊界以外的內(nèi)存。
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負(fù)重的操作。而且這種對象移動操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行。移動對象則內(nèi)存回收時會更復(fù)雜,不移動則內(nèi)存分配時會更復(fù)雜,從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至不需要停頓,但是從整個程序的吞吐量來看,移動對象會更劃算。關(guān)注吞吐量的Parallel Scavenge收集器是基于標(biāo)記-整理算法的,而關(guān)注延遲的CMS收集器則是基于標(biāo)記-清除算法的。標(biāo)記-整理算法清理過程如下圖所示:
在這里插入圖片描述

標(biāo)記-整理算法的優(yōu)點

標(biāo)記-整理算法會執(zhí)行壓縮,和其他算法相比而言,堆利用效率高。而且標(biāo)記-整理算法不會出現(xiàn)標(biāo)記-復(fù)制算法那樣只能利用半個堆的情況。另外,由于有了壓縮過程,不會產(chǎn)生碎片化。

標(biāo)記-整理算法的缺點

壓縮花費計算成本
標(biāo)記-清除算法中,清除階段也要搜索整個堆,不過搜索1次就夠了,但標(biāo)記-壓縮算法要搜索3次,這樣就要花費約3倍的時間,這是一個相當(dāng)巨大的缺陷,特別是堆越大,所消耗的成本也就越大。

保守式GC算法

前面提到了保守式,簡單的來說,保守式GC(Conservative GC)指的是"不能識別指針和非指針的GC"。
總結(jié)

本文簡單的介紹了JVM中幾個基本的垃圾回收算法,主要是標(biāo)記-清除算法,標(biāo)記-復(fù)制算法和標(biāo)記-整理算法。每個算法都有各自的優(yōu)缺點。一般而言新生代采用標(biāo)記-清除算法和標(biāo)記-復(fù)制算法居多,老年代會采用標(biāo)記-整理算法。




作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥