5張圖帶你徹底理解G1垃圾收集器
本文轉(zhuǎn)載自微信公眾號(hào)「程序員jinjunzhu」,作者jinjunzhu。轉(zhuǎn)載本文請(qǐng)聯(lián)系程序員jinjunzhu公眾號(hào)。
作為一款高效的垃圾收集器,G1在JDK7中加入JVM,在JDK9中取代CMS成為了默認(rèn)的垃圾收集器。
1 垃圾收集器回顧
1.1 新生代
新生代采用復(fù)制算法,主要的垃圾收集器有三個(gè),Serial、Parallel New 和 Parallel Scavenge,特性如下:
Serial:單線程收集器,串行方式運(yùn)行,GC 進(jìn)行時(shí),其他線程都會(huì)停止工作。在單核 CPU 下,收集效率最高。
Parallel New:Serial 的多線程版本,新生代默認(rèn)收集器。在多核 CPU 下,效率更高,可以跟CMS收集器配合使用。
Parallel Scavenge:多線程收集器,更加注重吞吐量,適合交互少的任務(wù),不能跟 CMS 配合使用。
1.2 老年代
Serial Old:采用標(biāo)記-整理(壓縮)算法,單線程收集。
Parallel Old:采用標(biāo)記-整理(壓縮)算法,可以跟 Parallel Scavenge 配合使用
CMS:Concurrent Mark Sweep,采用標(biāo)記-清除算法,收集線程可以跟用戶線程一起工作。
CMS缺點(diǎn):吞吐量低、無(wú)法處理浮動(dòng)垃圾、標(biāo)記清除算法會(huì)產(chǎn)生大量?jī)?nèi)存碎片、并發(fā)模式失敗后會(huì)切到Serial old。
G1:把堆劃分成多個(gè)大小相等的Region,新生代和老年代不再物理隔離,多核 CPU 和大內(nèi)存的場(chǎng)景下有很好的性能。新生代使用復(fù)制算法,老年代使用標(biāo)記-壓縮(整理)算法。
2 G1介紹
2.1 初識(shí)G1
G1垃圾收集器主要用于多處理器、大內(nèi)存的場(chǎng)景,它有五個(gè)屬性:分代、增量、并行(大多時(shí)候可以并發(fā))、stop the word、標(biāo)記整理。
- 分代:跟其他垃圾收集器一樣,G1把堆分成了年輕代和老年代,垃圾收集主要在年輕代,并且年輕代回收效率最高。偶爾也會(huì)在老年代進(jìn)行回收。
- 增量:為了讓垃圾收集時(shí)STW時(shí)間更短,G1采用增量和分步進(jìn)行回收。G1通過(guò)對(duì)應(yīng)用之前的行為和停頓時(shí)間進(jìn)行分析構(gòu)建出可預(yù)測(cè)停頓時(shí)間模型,并且利用這個(gè)信息來(lái)預(yù)測(cè)停頓時(shí)間內(nèi)的垃圾收集情況。比如:G1會(huì)首先回收那些收集效率高的內(nèi)存區(qū)域(這些區(qū)別大部分空間是可回收垃圾,這也是為啥叫G1的原因)。
- 并行和并發(fā):為了提高吞吐量,一些操作需要STW。一些需要花費(fèi)很多時(shí)間的操作,比如整堆操作(像全局標(biāo)記)可以并發(fā)執(zhí)行,同時(shí)可以并發(fā)跟應(yīng)用并行執(zhí)行。
- 標(biāo)記整理:G1主要使用標(biāo)記整理算法來(lái)進(jìn)行垃圾收集,把存活對(duì)象復(fù)制到一個(gè)新的區(qū)域,然后進(jìn)行壓縮,之前的區(qū)域就可以重新為新的對(duì)象分配空間了。如下圖:
我們知道,垃圾收集器的一個(gè)目標(biāo)就是STW(stop the word)越短越好。利用可預(yù)測(cè)停頓時(shí)間模型,G1為垃圾收集設(shè)定一個(gè)STW的目標(biāo)時(shí)間(通過(guò) -XX:MaxGCPauseMillis 參數(shù)設(shè)定,默認(rèn)200ms),G1盡可能地在這個(gè)時(shí)間內(nèi)完成垃圾收集,并且在不需要額外配置的情況下實(shí)現(xiàn)高吞吐量。
G1致力于在下面的應(yīng)用和環(huán)境下尋找延遲和吞吐量的最佳平衡:
- 堆大小達(dá)到10GB以上,并且一半以上的空間被存活的對(duì)象占用
- 隨著系統(tǒng)長(zhǎng)期運(yùn)行,對(duì)象分配和升級(jí)速率變化很快
- 堆中存在大量?jī)?nèi)存碎片
- 垃圾收集時(shí)停頓時(shí)間不能超過(guò)幾百毫秒,避免垃圾收集造成的長(zhǎng)時(shí)間停頓。
如果在JDK8中使用G1,我們可以使用參數(shù) -XX:+UseG1GC 來(lái)開(kāi)啟。
G1并不是一款實(shí)時(shí)收集器,它盡最大努力以高性能完成 MaxGCPauseMillis 設(shè)置的停頓時(shí)間,但并不能絕對(duì)保證在這個(gè)時(shí)間內(nèi)完成收集。
2.2 堆布局
G1把整個(gè)堆分成了大小相等的region,每一個(gè)region都是連續(xù)的虛擬內(nèi)存,region是內(nèi)存分配和回收的基本單位。如下圖:
紅色帶"S"的region表示新生代的survivor,紅色不帶"S"的表示新生代eden,淺藍(lán)色不帶"H"的表示老年代,淺藍(lán)色帶"H"的表示老年代中的大對(duì)象。跟G1之前的內(nèi)存分配策略不同的是,survivor、eden、老年代這些區(qū)域可能是不連續(xù)的。
G1在停頓的時(shí)候可以回收整個(gè)新生代的region,新生代region的對(duì)象要不復(fù)制到survivor區(qū)要不復(fù)制到老年代region。同時(shí)每次停頓都可以回收一部分老年代的內(nèi)存,把老年代從一個(gè)region復(fù)制到另一個(gè)region。
2.3 關(guān)于region
上一節(jié)我們看到,整個(gè)堆內(nèi)存被G1分成了多個(gè)大小相等的region,每個(gè)堆大約可以有2048個(gè)region,每個(gè)region大小為 1~32 MB(必須是2的次方)。region的大小通過(guò) -XX:G1HeapRegionSize 來(lái)設(shè)置,所以按照默認(rèn)值來(lái)G1能管理的最大內(nèi)存大約 32MB * 2048 = 64G。
2.4 大對(duì)象
大對(duì)象是指大小超過(guò)了region一半的對(duì)象,大對(duì)象可以橫跨多個(gè)region,給大對(duì)象分配內(nèi)存的時(shí)候會(huì)直接分配在老年代,并不會(huì)分配在eden區(qū)。
如下圖,一個(gè)大對(duì)象占據(jù)了兩個(gè)半region,給大對(duì)象分配內(nèi)存時(shí),必須從一個(gè)region開(kāi)始分配連續(xù)的region,在大對(duì)象被回收前,最后一個(gè)region不能被分配給其他對(duì)象。
大對(duì)象什么時(shí)候回收?通常,只有在mark結(jié)束以后的Cleanup停頓階段或者FullGC的時(shí)候,死亡的大對(duì)象才會(huì)被回收掉。但是,基本類型(比如bool數(shù)組、所有的整形數(shù)組、浮點(diǎn)型數(shù)組等)的數(shù)組大對(duì)象有個(gè)例外,G1會(huì)在任何GC停頓的時(shí)候回收這些死亡大對(duì)象。這個(gè)默認(rèn)是開(kāi)啟的,但是可以使用 -XX:G1EagerReclaimHumongousObjects 這個(gè)參數(shù)禁用掉。
分配大對(duì)象的時(shí)候,因?yàn)檎加每臻g太大,可能會(huì)過(guò)早發(fā)生GC停頓。G1在每次分配大對(duì)象的時(shí)候都會(huì)去檢查當(dāng)前堆內(nèi)存占用是否超過(guò)初始堆占用閾值IHOP(The Initiating Heap Occupancy Percent),如果當(dāng)前的堆占用率超過(guò)了IHOP閾值,就會(huì)立刻觸發(fā) initial mark。關(guān)于initial mark詳見(jiàn)第4節(jié)。
即使是在FullGC的時(shí)候,大對(duì)象也是永遠(yuǎn)不會(huì)被移動(dòng)的。這可能導(dǎo)致過(guò)早發(fā)生FullGC或者是意外的OOM,因?yàn)榇藭r(shí)雖然還有大量的空閑內(nèi)存,但是這些內(nèi)存都是region中的內(nèi)存碎片。
3 內(nèi)存分配
G1雖然把堆內(nèi)存劃分成了多個(gè)region,但是依然存在新生代和老年代的概念。G1新增了2個(gè)控制新生代內(nèi)存大小的參數(shù),-XX:G1NewSizePercent(默認(rèn)等于5),-XX:G1MaxNewSizePercent(默認(rèn)等于60)。也就是說(shuō)新生代大小默認(rèn)占整個(gè)堆內(nèi)存的 5% ~ 60%。
根據(jù)前面介紹,一個(gè)堆大概可以分配2048個(gè)region,每個(gè)region最大32M,這樣G1管理的整個(gè)堆的大小最大可以是64G,新生代占用的大小范圍是 3.2G ~ 38.4G。
對(duì)于 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent,下面幾個(gè)問(wèn)題需要注意:
- 如果設(shè)置了-Xmn,那這兩個(gè)參數(shù)是否生效?
生效,比如堆大小是64G,設(shè)置 -Xmn3.2G,那么就等價(jià)于 -XX:G1NewSizePercent=5 并且 -XX:G1MaxNewSizePercent=5,因?yàn)?.2G/64G = 5%。
- 如果設(shè)置了 -XX:NewRatio,這兩個(gè)參數(shù)是否生效?
生效,比如堆大小是64G,設(shè)置 -XX:NewRatio=3,那么就等價(jià)于 -XX:G1NewSizePercent=25 并且 -XX:G1MaxNewSizePercent=25。因?yàn)槟贻p代:老年代 = 1 :3,說(shuō)明年輕代占1/4 = 25%。
- 如果 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 只設(shè)置其中一個(gè),那這兩個(gè)參數(shù)還生效嗎?
設(shè)置的這個(gè)參數(shù)不生效,兩個(gè)參數(shù)都用默認(rèn)值。
- 如果-XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent 這兩個(gè)參數(shù)都生效了,什么時(shí)候動(dòng)態(tài)擴(kuò)容?
跟 -XX:GCTimeRatio 這個(gè)參數(shù)相關(guān)。這個(gè)參數(shù)為0~100之間的整數(shù)(G1默認(rèn)是9, 其它收集器默認(rèn)是99),值為 n 則系統(tǒng)將花費(fèi)不超過(guò) 1/(1+n) 的時(shí)間用于垃圾收集。因此G1默認(rèn)最多 10% 的時(shí)間用于垃圾收集,如果垃圾收集時(shí)間超過(guò)10%,則觸發(fā)擴(kuò)容。如果擴(kuò)容失敗,則發(fā)起Full GC。
4 垃圾回收
G1的垃圾收集是在 Young-Only 和 Space-Reclamation兩個(gè)階段交替執(zhí)行的。如下圖:
young-only階段會(huì)用對(duì)象逐步把老年代區(qū)域填滿,space-reclamation階段除了會(huì)回收年輕代的內(nèi)存以外,還會(huì)增量回收老年代的內(nèi)存。完成后重新開(kāi)始young-only階段。
4.1 Young-only
Young-only階段流程如下圖:
這個(gè)階段從普通的 young-only GC 開(kāi)始,young-only GC把一些對(duì)象移動(dòng)到老年代,當(dāng)老年代的空間占用達(dá)到IHOP時(shí),G1就停止普通的young-only GC,開(kāi)始初始標(biāo)記(Initial Mark)。
- 初始標(biāo)記:這個(gè)過(guò)程除了普通的 young-only GC 外,還會(huì)開(kāi)始并發(fā)標(biāo)記過(guò)程,這個(gè)過(guò)程決定了被標(biāo)記的老年代存活對(duì)象在下一次space-reclamation階段會(huì)被保留。這個(gè)過(guò)程不會(huì)STW,有可能標(biāo)記還沒(méi)有結(jié)束普通的 young-only GC 就開(kāi)始了。這個(gè)標(biāo)記過(guò)程需要在重新標(biāo)記(Remark)和清理(Cleanup)兩個(gè)過(guò)程后才能結(jié)束。
- 重新標(biāo)記: 這個(gè)過(guò)程會(huì)STW,這個(gè)過(guò)程做全局引用和類卸載。
- 在重新標(biāo)記和清理這兩個(gè)階段之間G1會(huì)并發(fā)計(jì)算對(duì)象存活信息,這個(gè)信息用于清理階段更新內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
- 清理階段:
這個(gè)節(jié)點(diǎn)回收所有的空閑區(qū)域,并且決定是否接著執(zhí)行一次space-reclamation,如果是,則僅僅執(zhí)行一次單獨(dú)的young-only GC,young-only階段就結(jié)束了。
關(guān)于IHOP,默認(rèn)情況下,G1會(huì)觀察標(biāo)記周期內(nèi)標(biāo)記花了多少時(shí)間,老年代分配了多少內(nèi)存,以此來(lái)自動(dòng)確定一個(gè)最佳的IHOP,這叫做自適應(yīng)IHOP。如果開(kāi)啟這個(gè)功能,因?yàn)槌跏紩r(shí)沒(méi)有足夠的觀察數(shù)據(jù)來(lái)確定IHOP,G1會(huì)用參數(shù) -XX:InitiatingHeapOccupancyPercent 來(lái)指定初始IHOP??梢杂?-XX:-G1UseAdaptiveIHOP 參數(shù)關(guān)閉自適應(yīng)IHOP,這樣IHOP就參數(shù) -XX:InitiatingHeapOccupancyPercent 指定的固定值。自適應(yīng)IHOP這樣設(shè)置老年代占有率,當(dāng)老年代占有率=老年代最大占有率 - 參數(shù) -XX:G1HeapReservePercent 值時(shí),啟動(dòng)space-reclamation階段的第一個(gè)Mixed GC。這里參數(shù) -XX:G1HeapReservePercent 作為一個(gè)額外的緩存值。
關(guān)于標(biāo)記,標(biāo)記使用 SATB 算法,初始標(biāo)記開(kāi)始時(shí),G1保存堆的一份虛擬鏡像,這份鏡像存活的對(duì)象在后續(xù)的標(biāo)記過(guò)程中也被認(rèn)為是存活的。這有一個(gè)問(wèn)題,就是標(biāo)記過(guò)程中如果部分對(duì)象死亡了,對(duì)于 space-reclamation 階段來(lái)說(shuō)它們?nèi)匀皇谴婊畹?也有少部分例外)。跟其他垃圾收集器相比,這會(huì)導(dǎo)致一部分死亡對(duì)象被錯(cuò)誤保留,但是為標(biāo)記階段提供了更好的吞吐量,而且這些錯(cuò)誤保留的對(duì)象會(huì)在下一次標(biāo)記階段被回收。
在young-only階段,要回收新生代的region。每一次 young-only 結(jié)束的時(shí)候,G1總是會(huì)調(diào)整新生代大小。G1可以使用參數(shù) -XX:MaxGCPauseTimeMillis和 -XX:PauseTimeIntervalMillis 來(lái)設(shè)置目標(biāo)停頓時(shí)間,這兩個(gè)參數(shù)是對(duì)實(shí)際停頓時(shí)間的長(zhǎng)期觀察得來(lái)的。他會(huì)根據(jù)在GC的時(shí)候要拷貝多少個(gè)對(duì)象,對(duì)象之間是如何相互關(guān)聯(lián)的等信息計(jì)算出來(lái)回收相同大小的新生代內(nèi)存需要花費(fèi)多少時(shí)間,
如果沒(méi)有其他的限定條件,G1會(huì)把young區(qū)的大小調(diào)整為 -XX:G1NewSizePercent和 -XX:G1MaxNewSizePercent 之間的值來(lái)滿足停頓時(shí)間的要求。
4.2 Space-reclamation
這個(gè)階段由多個(gè)Mixed GC組成,不光回收年輕代垃圾,也回收老年代垃圾。當(dāng) G1 發(fā)現(xiàn)回收更多的老年代區(qū)域不能釋放更多空閑空間時(shí),這個(gè)階段結(jié)束。之后,周期性地再次開(kāi)啟一個(gè)新的Young-only階段。
當(dāng)G1收集存活對(duì)象信息時(shí)內(nèi)存不足,G1會(huì)做一個(gè)Full GC,并且會(huì)STW。
在 space-reclamation 階段,G1會(huì)盡量在GC停頓時(shí)間內(nèi)回收盡可能多的老年代內(nèi)存。這個(gè)階段新生代內(nèi)存大小被調(diào)整為 -XX:G1NewSizePercent 設(shè)置的允許的最小值,只要存在可回收的老年代region就會(huì)被添加到回收集合中,直到再添加會(huì)超出目標(biāo)停頓時(shí)間為止。在特定的某個(gè)GC停頓時(shí)間內(nèi),G1會(huì)按照這老年代region回收的效率(效率高的優(yōu)先收集)和剩余可用時(shí)間來(lái)得到最終待回收region集合。
每一個(gè)GC停頓期間要回收的老年代region數(shù)量受限于候選region集合數(shù)量除以 -XX:G1MixedGCCountTarget 這個(gè)參數(shù)值,參數(shù) -XX:G1MixedGCCountTarget 指定一個(gè)周期內(nèi)觸發(fā)Mixed GC最大次數(shù),默認(rèn)值8。比如 -XX:G1MixedGCCountTarget 采用默認(rèn)值8,候選region集合有200個(gè)region,那每次停頓期間收集25個(gè)region。
候選region集合是老年代中所有占用率低于 -XX:G1MixedGCLiveThresholdPercent 的region。
當(dāng)待回收region集合中可回收的空間占用率低于參數(shù)值 -XX:G1HeapWastePercent 的時(shí)候,Space-Reclamation結(jié)束。
4.3 內(nèi)存緊張情況
當(dāng)應(yīng)用存活對(duì)象占用了大量?jī)?nèi)存,以至于回收剩余對(duì)象沒(méi)有足夠的空間拷貝時(shí),就會(huì)觸發(fā) evacuation failure。這時(shí)G1為了完成當(dāng)前的垃圾收集,會(huì)保留已經(jīng)位于新的位置上的存活對(duì)象不動(dòng),對(duì)于沒(méi)有移動(dòng)和拷貝的對(duì)象就不會(huì)進(jìn)行拷貝了,僅僅調(diào)整對(duì)象間的引用。
evacuation failure會(huì)導(dǎo)致一些額外的開(kāi)銷,但是一般會(huì)跟其他 young GC 一樣快。evacuation failure完成以后,G1會(huì)跟正常情況下一樣繼續(xù)恢復(fù)應(yīng)用的執(zhí)行。G1會(huì)假設(shè) evacuation failure是發(fā)生在GC的后期,這時(shí)大部分對(duì)象已經(jīng)移動(dòng)過(guò)了,并且已經(jīng)有足夠的內(nèi)存來(lái)繼續(xù)執(zhí)行應(yīng)用程序一直到 mark 結(jié)束 space-reclamation 開(kāi)始。如果這個(gè)假設(shè)不成立(也就是說(shuō)沒(méi)有足夠的內(nèi)存來(lái)執(zhí)行應(yīng)用程序),G1最終只能發(fā)起Full GC,對(duì)整個(gè)堆做壓縮,這個(gè)過(guò)程可能會(huì)非常慢。
5 跟其他收集器比較
5.1 Parallel GC
Parallel GC 可以壓縮和回收老年代的內(nèi)存,但是也只能對(duì)老年代整體來(lái)操作。G1以增量的方式把整個(gè)GC工作增量的分散到多個(gè)更短的停頓時(shí)間中,當(dāng)然這可能會(huì)犧牲一定吞吐量。
5.2 CMS
跟CMS類似,G1并發(fā)回收老年代內(nèi)存,但是,CMS采用標(biāo)記-清除算法,不會(huì)處理老年代的內(nèi)存碎片,最終就會(huì)導(dǎo)致長(zhǎng)時(shí)間的FullGC。
5.3 G1問(wèn)題
因?yàn)椴捎貌l(fā)收集,G1的性能開(kāi)銷會(huì)更大,這可能會(huì)影響吞吐量。
5.4 G1優(yōu)勢(shì)
G1在任何的GC期間都可以回收老年代中全空或者占用大空間的內(nèi)存。這可以避免一些不必要的GC,因?yàn)榭梢苑浅]p易地釋放大量的內(nèi)存空間。這個(gè)功能默認(rèn)開(kāi)啟,可以采用 -XX:-G1EagerReclaimHumongousObjects 參數(shù)關(guān)閉。
G1可以選擇對(duì)整個(gè)堆里面的String進(jìn)行并行去重。這個(gè)功能默認(rèn)關(guān)閉,可以使用參數(shù) -XX:+G1EnableStringDeduplication 來(lái)開(kāi)啟。
6 總結(jié)
本文詳細(xì)介紹了G1垃圾收集器,希望能夠?qū)δ憷斫釭1有所幫助。
參考:
https://docs.oracle.com/javase/10/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-CE6F94B6-71AF-45D5-829E-DEADD9BA929D
https://mp.weixin.qq.com/s/KkA3c2_AX6feYPJRhnPOyQ