深入淺出JVM垃圾回收器
引言
程序的運(yùn)行必然需要申請內(nèi)存資源,使用結(jié)束后的內(nèi)存資源如果不及時釋放就會造成內(nèi)存中的垃圾越來越多,最終造成內(nèi)存溢出,而垃圾回收就是把無用的內(nèi)存垃圾清理掉,這樣內(nèi)存就可以被程序反復(fù)使用。
垃圾回收(Garbage Collection 簡稱GC)是Java體系最重要的組成部分之一,和C/C++的手工內(nèi)存管理方式不同,JVM虛擬機(jī)提供了一套全自動的內(nèi)存管理方案,以減少開發(fā)人員在內(nèi)存管理方面的相關(guān)工作。
(一) 常見的垃圾回收算法和垃圾回收器
1. 常見的垃圾回收算法
a) 標(biāo)記清除算法(Mark-Sweep)
最早出現(xiàn)也是最基礎(chǔ)的垃圾回收算法,算法整體分為兩個階段“標(biāo)記”和“清除”,首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對象,該算法簡單快速,但是缺點明顯:一是標(biāo)記和清除兩個過程的效率都不高。二是清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片。內(nèi)存碎片過多可能導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次垃圾回收動作。圖1中展示了標(biāo)記-清楚算法的過程。
圖 1
b) 標(biāo)記復(fù)制算法(Copying)
為了解決標(biāo)記清除算法的大對象回收效率和內(nèi)存碎片化問題。提出了另一種“半?yún)^(qū)復(fù)制”的算法,核心思想就是將原有的內(nèi)存空間分為兩塊,每次只使用一半?yún)^(qū)域。垃圾回收時將使用的對象復(fù)制到未使用的半?yún)^(qū)中,之后清除當(dāng)前使用半?yún)^(qū)的所有對象,最后交換兩個內(nèi)存角色,完成回收工作。雖然解決了內(nèi)存碎片化的問題,但是如果活動對象較多,就會導(dǎo)致復(fù)制的對象過多,復(fù)制的成本很高且僅能使用一半的內(nèi)存,因此單純的復(fù)制算法也有很多問題。圖2展示了復(fù)制算法的過程。
圖 2
c) 標(biāo)記壓縮算法(Mark-Compact)
標(biāo)記壓縮算法的標(biāo)記過程與標(biāo)記清除算法一樣,但是后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向空閑內(nèi)存的一端移動,然后直接清理掉邊界以外的所有內(nèi)存,這種方法避免了內(nèi)存碎片的產(chǎn)生,又不需要兩塊相同的內(nèi)存空間,標(biāo)記壓縮算法的最終效果等于標(biāo)記清除執(zhí)行后再進(jìn)行一次內(nèi)存碎片整理。圖3展示了標(biāo)記壓縮算法的過程。
圖3
2.分代回收理論(GenerationalCollecting)
分代回收是一種回收思想,目前被虛擬機(jī)廣泛使用,在前面介紹的算法中,沒有一種算法可以完全替代其他算法,分代收集就是基于這種思想,將內(nèi)存根據(jù)對象的特點分成幾塊,根據(jù)每塊內(nèi)存的特點使用不同的回收算法,提高內(nèi)存的回收效率。主流的JVM虛擬機(jī)里面一般會把JAVA堆內(nèi)存劃分為年輕代(Young Generation)和老年代(Old Generation)兩個區(qū)域,每次垃圾收集時會有大批的對象被回收,少量存活的對象將逐步轉(zhuǎn)移到老年代存放。圖4展示了主流JVM虛擬機(jī)的內(nèi)存的分代情況。
圖 4
3. 常見的垃圾回收器
再說垃圾回收器之前,需要再說一下為什么需要不斷優(yōu)化垃圾回收器,一切都源于一個詞語“Stop The World”簡稱STW,JVM虛擬機(jī)會自動發(fā)起和自動完成回收垃圾的工作,用戶在不可干預(yù)的情況下,需要暫停所有正常工作線程來等待垃圾回收的完成。試想下每工作幾小時就需要暫停幾分鐘,這樣的程序是無法讓人接受的。
垃圾回收的算法為垃圾回收器提供了理論基礎(chǔ),垃圾回收器就是這些理論算法的具體實現(xiàn)。圖5展示了七種不同的垃圾回收器,如果兩回收器之前存在連線,就說明可以搭配使用。
圖5
a) 串行回收器(Serial + Serial Old)
最古老的垃圾回收器,也是最基本的垃圾回收器之一,是一個單線程的垃圾回收器,在年輕代工作時使用的是標(biāo)記復(fù)制算法,在老年代工作時使用的是標(biāo)記壓縮算法。在CPU性能受限的情況下,它的性能表現(xiàn)依然很優(yōu)秀。圖6展示了串行垃圾回收器的回收過程。
圖6
b) 并發(fā)回收器(ParNew和CMS)
ParNew回收器是一款只能工作在年輕代的并行收集器 ,它是Serial收集器的多線程版本,由于使用多線程進(jìn)行垃圾回收,在計算能力較強(qiáng)的CPU上,產(chǎn)生的停頓時間要小于串行回收器。圖7展示了ParNew并行的回收的過程。
圖7
CMS(Concurrent Mark Sweep)是一款只能工作在老年代的收集器,第一款設(shè)計較為的復(fù)雜的收集器,也是JVM虛擬機(jī)追求低停頓的第一次嘗試,但是也有明顯的缺點,圖8展示了CMS收集器的回收過程。總的來說有三點:首先CMS收集器對CPU性能比較敏感,如果CPU性能不足或者本身的負(fù)載就很高,那這會讓整個垃圾回收的過程變長。其次,在并發(fā)標(biāo)記和并發(fā)清除的階段,用戶線程會有新的垃圾產(chǎn)生,就會產(chǎn)生“浮動垃圾(Floating Garbage)”,所以就不能像其他回收器那樣等到老年代100%再進(jìn)行回收,需要預(yù)留一部分內(nèi)存提供給用戶線程使用。最后,CMS是一個基于標(biāo)記清除算法實現(xiàn)的回收器,這就會產(chǎn)生大量的內(nèi)存碎片,如果有大對象需要處理,碎片過多時就需要對Old區(qū)再進(jìn)行一次垃圾回收進(jìn)行內(nèi)存整理。ParNew和CMS垃圾回收器一般搭配來進(jìn)行使用,不過這兩個收集器已經(jīng)在JDK9中被標(biāo)記為廢棄,JDK14該回收器將被正式刪除。
圖8
c) 并行回收(ParallelGC+ParallelOldGC)
ParallelGC和ParallelOldGC是JDK8中默認(rèn)使用的兩個回收器分別用在年輕代和老年代, 并且他們都是多線程回收器。ParallelGC采用的是復(fù)制算法進(jìn)行垃圾回收,它和ParNew不同的是可以控制系統(tǒng)的吞吐量和最大停頓時間,并且增加了自調(diào)優(yōu)的功能,相當(dāng)于ParNew的升級版本。ParallelOldGC使用的是標(biāo)記壓縮算法,這個回收器在JDK6時開始提供使用。圖9展示了ParallelGC和ParallelOldGC的回收過程。
圖9
d)分區(qū)回收器(Garbage First)
隨著大數(shù)據(jù)時代的來臨,JVM虛擬機(jī)的內(nèi)存也越來越大,在相同條件下,內(nèi)存空間越大,一次GC所需的時間就越長,產(chǎn)生的停頓就越長。為了更好的控制GC產(chǎn)生的STW時間。Garbage First回收器(簡稱G1)出現(xiàn)了,JDK6時開始推出試驗版本,JDK7 Update4中逐漸的成熟起來,終于在JDK8 Update40以后G1提供并發(fā)的類卸載功能成為了可以替代CMS的回收器,JDK9版本中G1被設(shè)置成默認(rèn)的垃圾回收器。G1回收器引入了分區(qū)(Region)的概念,將整個內(nèi)存空間分為不同大小的小分區(qū),每個小分區(qū)單獨(dú)使用,獨(dú)立回收。不過G1也還是遵循了分代回收的理論,還是會區(qū)分年輕代和老年代的概念,從整體看G1是基于標(biāo)記壓縮算法實現(xiàn)的,但是從局部看每個分區(qū)之間又是基于標(biāo)記復(fù)制算法實現(xiàn)的。
圖10
(二) 垃圾回收器內(nèi)存分配詳解
1. 分代垃圾回收器
分代的垃圾回收器是如何進(jìn)行內(nèi)存分配和管理的呢?我們再來回顧下分代思想。如圖11所示,整個的JVM空間被分成2個區(qū)域年輕代(Young Generation)和老年代(Old Generation),而Young區(qū)又被分成了伊甸園區(qū)(Eden,統(tǒng)簡稱Eden)和生存區(qū)(Survivor),而Survivor又被分為From(Survivor0,統(tǒng)簡稱“S0”)和To(Survivor1,統(tǒng)簡稱“S1”)兩個區(qū)域。年輕代和老年代比例為1:2(默認(rèn)參數(shù)),在年輕代中內(nèi)存中又被分成了三份(默認(rèn)為8:1:1)。
G行已經(jīng)開始逐步開始從JDK6向JDK8進(jìn)行替換,關(guān)于這部分內(nèi)容主要針對JDK8版本進(jìn)行說明。
圖 11
幾乎所有新生成的對象首先都是放在年輕代,大部分對象在 Eden 區(qū)中生成,當(dāng)Eden區(qū)內(nèi)存空間不足時,則會發(fā)起一次GC,回收器會將Eden區(qū)存活對象復(fù)制到S0,然后清空Eden區(qū)。如圖12展示的過程。
圖 12
下一次Eden區(qū)空間不足時,會將Eden區(qū)和S0區(qū)的存活對象復(fù)制到S1區(qū),然后清空Eden區(qū)和S0區(qū)。如圖13展示的過程。
圖 13
這時候會又出一個問題,對象什么時候去老年代呢?對象每次在S0和S1之間復(fù)制一次,這個對象的年齡就長一歲,當(dāng)15歲(默認(rèn)為15歲,可通過參數(shù)調(diào)整)之后這個對象就會被復(fù)制到老年代去。如圖14展示的過程。
圖 14
如此這樣循環(huán)往復(fù),當(dāng)老年代也空間不足時,回收器就會用對老年代進(jìn)行回收來釋放內(nèi)存空間,也就是通常說的Full GC。
2. 分區(qū)垃圾回收器
傳統(tǒng)的GC收集器將連續(xù)的內(nèi)存空間劃分為新生代、老年代和永久代(JDK 8去除了永久代,引入了元空間Metaspace)。如下圖15所示,不過現(xiàn)在請大家忘記它吧。
圖 15
G1的內(nèi)存存儲地址是不連續(xù)的,G1 將連續(xù)的Java堆劃分為多個大小相等的獨(dú)立區(qū)域(Region),每一個Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代Old空間,每個Region的大小可以取值范圍為1MB~32MB,且應(yīng)為2的N次冪,并且新增一個區(qū)域叫巨大對象(humongous object,H-obj),只要大小超過了一個Region容量一半即可判定為大對象,直接放入大對象區(qū)。對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續(xù)的Humongous Region之中。如下圖16所示G1內(nèi)存的分配情況。
圖 16
在分配一般對象時,當(dāng)所有Eden Region使用達(dá)到最大閾值并且無法申請足夠內(nèi)存時,會觸發(fā)一次年輕代Region的GC。每次GC會回收所有Eden以及Survivor,并且將存活對象復(fù)制到空白的Survivor區(qū)。如下圖17所示。
圖 17
那內(nèi)存什么時候進(jìn)入老年代的Region呢?在G1回收器中有兩種情況會進(jìn)入到老年代Region:
同分代回收的規(guī)則,內(nèi)存每在年輕代的Region被復(fù)制一次,年齡就長一歲,當(dāng)15歲(默認(rèn)為15歲,可通過參數(shù)調(diào)整)之后這個對象就會被復(fù)制到老年代的Region。
動態(tài)年齡判斷規(guī)則,某次年輕代GC 過后,發(fā)現(xiàn) Survivor 區(qū)中相同年齡的對象達(dá)到了 Survivor 的 50%,那么該年齡及以上的對象,會被直接移動到老年代中。例如Survivor 區(qū)中存在年齡分別為 1、2、3、4 的對象,而年齡為 3 的對象超過了 Survivor 區(qū)的 50%,那么年齡大于等于 3 的對象,就會被全部移動到老年代的Region。
最后再談下分區(qū)回收獨(dú)有的混合回收(Mixed GC),在G1中不存在單獨(dú)回收老年代Region的行為,而是當(dāng)要發(fā)生老年代的回收時,同時也會對新生代以及大對象進(jìn)行回收,因此這個階段稱之為混合回收。當(dāng)老年代Region的使用率占比達(dá)到 45%時,就會觸發(fā)混合回收。
不過在G1中Full GC還是存在的,如果空閑的 Region 大小無法放得下存活對象的內(nèi)存大小時系統(tǒng)就不得不暫停應(yīng)用程序,進(jìn)行一次 Full GC。進(jìn)行 Full GC 時采用的是單線程進(jìn)行標(biāo)記、清理和整理內(nèi)存,這個過程是非常漫長的,因此應(yīng)該盡量避免 Full GC 的觸發(fā)。
(三) 垃圾回收器的優(yōu)化思路
垃圾回收器的優(yōu)化思路
垃圾回收器的選擇是JVM優(yōu)化的一個重要配置,選擇合適的垃圾回收器可以讓JVM性能有一個很大的提升。其實JVM調(diào)優(yōu)主要是調(diào)整兩個指標(biāo):
JVM虛擬機(jī)停頓時間(Stop The World)
吞吐量是指CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運(yùn)行用戶代碼時間 /( 運(yùn)行用戶代碼時間 + 垃圾收集時間 )
下面分享下關(guān)于回收器選擇上的一些經(jīng)驗。
1. 小內(nèi)存,默認(rèn)優(yōu)先:
大部分應(yīng)用JVM堆內(nèi)存都在4G以內(nèi),優(yōu)先使用JDK8默認(rèn)的垃圾回收器。如今大部分系統(tǒng)都運(yùn)行在虛擬機(jī)上,G1固然是更先進(jìn)的垃圾回收器,但是G1在垃圾回收時產(chǎn)生的內(nèi)存占用也更高,所以小內(nèi)容使用G1作為回收器會增加GC的次數(shù),吞吐量會下降。
2. 大內(nèi)存,G1優(yōu)先:
當(dāng)內(nèi)存大于8G后,應(yīng)該優(yōu)先考慮G1垃圾回收器,因為當(dāng)內(nèi)存增大后,在進(jìn)行垃圾回收時會將對象從s0復(fù)制到s1內(nèi)存越大,復(fù)制的時間越長,會增加系統(tǒng)STW的時間,導(dǎo)致系統(tǒng)的停頓時間過長。
總結(jié)
隨著Java的不斷發(fā)展,有很多新的回收器出現(xiàn),如:shenandoahGC和ZGC,同為新一代的低延遲收集器, 分別由RedHat和Oracle開發(fā), 不過還在實驗階段, 尚未使用于生產(chǎn)環(huán)境,針對不同類型的應(yīng)用Java提供了多種垃圾回收策略。
本文對Java虛擬機(jī)垃圾回收器及其回收策略進(jìn)行逐一介紹,同時對垃圾回收的優(yōu)化思路做一些簡單討論,以期讀者能對Java虛擬機(jī)的垃圾回收增加理解,同時對垃圾回收的優(yōu)化有一些初步認(rèn)識,為后續(xù)工作中的Java應(yīng)用調(diào)優(yōu)打下基礎(chǔ)。