深入解析 JVM 中的 G1 垃圾回收器
G1(garbage first)垃圾回收器作為jdk9默認(rèn)的垃圾回收器,其設(shè)計(jì)理念完全綜合了cms和parallel scavenge的優(yōu)點(diǎn),采用了一種獨(dú)特的內(nèi)存管理策略,實(shí)現(xiàn)了整堆維度的內(nèi)存管理,本文針對該垃圾回收器進(jìn)行一個(gè)較為綜合全面的分析,希望對你有幫助。
一、G1垃圾回收器基本概念
1. 設(shè)計(jì)理念
G1相較于傳統(tǒng)垃圾回收器而言,它并沒有明確的內(nèi)存區(qū)域的劃分,即在給定的一塊固定的內(nèi)存空間后,G1垃圾回收器將其以region為最小單位進(jìn)行劃分,默認(rèn)情況下,每個(gè)region的大小是基于給定堆內(nèi)存大小除2048從而得出結(jié)果。假設(shè)我們分配堆內(nèi)存為4G,那么對應(yīng)的堆內(nèi)存的每個(gè)region大小就是4096/2048,也就是2M,當(dāng)然region的大小我們也可以通過參數(shù)進(jìn)行設(shè)定。
region的特別之處在于它可以是堆內(nèi)存中的任何角色,它可以作為Eden,也可以是survivor或者是old,需要注意的是G1有些特殊區(qū)域,當(dāng)對象大小大于region大小的50%的情況下,g1就判定這個(gè)對象為大對象,這些對象就會(huì)分配到一個(gè)老年代一個(gè)連續(xù)的堆內(nèi)存空間即Humongous region內(nèi)存區(qū)中。
相較于cms和傳統(tǒng)的parallel scavenge,它汲取了二者的優(yōu)點(diǎn),它并不像傳統(tǒng)垃圾回收器那樣等到堆內(nèi)存空間快滿了再進(jìn)行垃圾回收。而是提出少量垃圾多次回收保證短耗時(shí)高吞吐的概念,通過這個(gè)一個(gè)基調(diào)使得g1有著如下幾個(gè)特點(diǎn):
- 充分利用CPU資源:因?yàn)樗翘崆斑M(jìn)行少量多次的垃圾回收,這也就意味著該垃圾回收器會(huì)盡可能利用CPU資源進(jìn)行并行的垃圾回收,提升CPU資源利用率,也保證垃圾回收器良好的性能表現(xiàn)。
- 盡可能少的內(nèi)存碎片:g1無論是新生代還是老年代,都一律采用高效的標(biāo)記復(fù)制法,解決cms垃圾回收器遺留的大量內(nèi)存碎片問題,從而提升內(nèi)存利用率。
- 暫停時(shí)間短:g1支持在用戶給定的最大暫停時(shí)間內(nèi)完成一次垃圾回收,由此避免了長時(shí)間的STW,保證程序的高響應(yīng)。
通過上述的這些設(shè)計(jì)理念,它在吞吐量、內(nèi)存管理上都有比較明顯的優(yōu)勢,這也是為什么JDK9將G1作為默認(rèn)垃圾回收器的原因(JDK8最新版G1也已經(jīng)比較穩(wěn)定,同樣建議在堆內(nèi)存分配大于4G的情況下,采用G1垃圾回收器)。
2. 常見參數(shù)
g1垃圾回收器有下面幾個(gè)比較常用的參數(shù),首先自然是配置垃圾回收器:
// 整堆啟用G1的垃圾回收器
-XX:+UseG1GC
然后就是設(shè)置最大的STW時(shí)間,默認(rèn)情況下是200ms:
# 設(shè)置最大暫停時(shí)間(默認(rèn)200ms)
-XX:MaxGCPauseMillis=n
還有就是設(shè)置Region的內(nèi)存大?。?/p>
# 指定Region的內(nèi)存大小,n必須是2的指數(shù)冪,其取值范圍是從1M到32M
-XX:G1HeapRegionSize=n
設(shè)置垃圾回收線程數(shù):
# 指定垃圾回收工作的線程數(shù)量
-XX:ParallelGCThreads=n
二、詳解GC中的Young GC
1. Young GC工作流程
和常規(guī)的新生代算法過程一樣,G1垃圾回收器新生代回收也只是針對Eden區(qū)和survivor區(qū),默認(rèn)情況下,當(dāng)整堆內(nèi)存空間中Eden區(qū)使用率超過60%或回收時(shí)間接近用戶設(shè)定的最大STW時(shí)間時(shí),就會(huì)觸發(fā)Young GC,通過標(biāo)記復(fù)制法將無用的過期對象回收,同時(shí)將存活的對象復(fù)制到另外的survivor區(qū)中(年齡加上1),對于年齡達(dá)到閾值的(默認(rèn)15)會(huì)直接晉升到老年代。
后續(xù)這塊Eden區(qū)就會(huì)被清空變?yōu)橐粔K空閑region并維護(hù)到region空閑池中等待后續(xù)被分配使用:
2. Young GC如何解決跨代引用問題
我們已經(jīng)從一個(gè)比較宏觀的角度說明的新生代回收的流程,這里我們來聊一個(gè)新生代回收時(shí)需要注意的一個(gè)問題——跨代引用。 跨代引用一直是垃圾回收器一個(gè)老生常談的問題了,無論是新生代還是老年代進(jìn)行垃圾回收的時(shí)候,對應(yīng)的內(nèi)存區(qū)域都無法感知對方是否持自己內(nèi)存區(qū)域的對象,同時(shí)考慮到g1垃圾回收器在物理空間排布上,新生代和老年代還是不連續(xù)的,所以對于跨代問題就顯得更加棘手了。
對此g1提出了一個(gè)卡頁、卡表、記憶集、寫屏障幾個(gè)重要的概念概念,我們先來說說卡頁的概念,為了方便后續(xù)后續(xù)記憶集的維護(hù),g1將每個(gè)region都進(jìn)行了更小維度的切割將其稱為頁并加上編號這就是卡頁(Card Page)。
可以看到筆者在上圖中留了一塊空間,這就是我們需要了解的第二個(gè)概念——卡表(Card Table),在物理實(shí)現(xiàn)上它是在每個(gè)region中預(yù)留一個(gè)空間并用數(shù)組實(shí)現(xiàn),記錄持有當(dāng)前region的非回收區(qū)域老年代對于新生代對象的持有情況,例如有了老年代區(qū)域的11號卡頁上某個(gè)對象持有當(dāng)前新生代region的某個(gè)對象,對應(yīng)的我們的region的記憶集就會(huì)將數(shù)組索引10(11號對應(yīng)索引0)標(biāo)記為1,代表卡頁11這個(gè)區(qū)域持有我們的對象,而數(shù)組對應(yīng)索引未知的元素也被稱為臟卡。
如此一來,進(jìn)行新生代回收時(shí),通過遍歷即得到對應(yīng)的臟卡構(gòu)成一個(gè)跨代引用的記憶集,記憶集中對應(yīng)的老年代對象也會(huì)被作為GC root進(jìn)行可達(dá)性算法分析,保證跨代引用分析的準(zhǔn)確性。
通過記憶集解決了各個(gè)region跨代引用關(guān)系的維護(hù)之后,我們就需要考慮并發(fā)一致性的問題,例如:當(dāng)前這個(gè)老年代的卡頁對象引用這個(gè)新生代對象,在多線程并發(fā)操作過程中這個(gè)老年代對象就放棄了對這個(gè)新生代對象的引用,此時(shí)我們就需要找到一個(gè)手段盡可能保證記憶集的準(zhǔn)確性,這就是我們要提到的最后一個(gè)概念——寫屏障。 我們還是以上文一個(gè)老年代region引用持有新生代對象為例,當(dāng)這個(gè)老年代region持有了一個(gè)新生代對象時(shí),除了會(huì)將新生代的記憶集標(biāo)記為1設(shè)為臟卡以外,還會(huì)通過一個(gè)寫后屏障將當(dāng)前老年代region的臟卡寫入到一個(gè)臟卡隊(duì)列中,交由g1回收器的某個(gè)異步線程輪詢處理,以保證每個(gè)region的記憶集能夠盡可能拿到最新的結(jié)果。
三、詳解G1中的Mixed GC
1. Mixed GC工作過程
mixed gc是g1垃圾回收器中的一個(gè)獨(dú)有的概念,它是一種混合回收的垃圾回收模式,該模式遇到以下幾個(gè)條件時(shí)就會(huì)觸發(fā):
- 新生代分配大對象時(shí)。
- 老年代內(nèi)存空間占有率率達(dá)到45%。
進(jìn)行混合回收時(shí),它會(huì)回收所有年輕代和一部分老年代,這里部分的老年代region的選取策略是找到垃圾對象最多的那部分region,以保證在有限的暫停時(shí)間內(nèi)做到最有性價(jià)比的垃圾回收。
2. Full GC和Mixed GC的區(qū)別
這里我們需要補(bǔ)充說明一下mixed gc和full gc的一點(diǎn)區(qū)別,可能通過上述描述提及mixed gc涉及新生代和老年代的空間回收,導(dǎo)致很多讀者認(rèn)為mixed gc和full gc是一個(gè)概念,其實(shí)并不是,原因如下:
- mixed gc是在整堆空間利用率到達(dá)45%時(shí)觸發(fā)的,而full gc則是mixed gc在進(jìn)行清理之后仍然無法給出空閑區(qū)域分配對象的一種退化擔(dān)保策略。
- 從回收的過程來說,mixed gc在宏觀上是并行回收的(少部分階段會(huì)進(jìn)行停頓),而full gc則是采用serial old垃圾回收器進(jìn)行一個(gè)完完全全是STW的垃圾回收,且回收過程采用的是標(biāo)記整理法,耗時(shí)較長。
3. Mixed GC工作原理
我們都知道Mixed GC是一種混合的垃圾回收模式,涉及到老年代的回收,由于老年代存在較多的對象,所以為保證老年代垃圾回收的效率,減少STW的時(shí)長,g1設(shè)計(jì)的垃圾回收通過一種三色標(biāo)記的并行垃圾回收技術(shù)來做到這一點(diǎn),整體步驟為:
- 初始標(biāo)記:該階段會(huì)掃描當(dāng)前堆內(nèi)存中所有的GC Root及其關(guān)聯(lián)的對象將其標(biāo)為灰色(意意為該對象在GC Root的引用鏈上,但該對象的所有引用對象都還未標(biāo)記過),會(huì)有短暫的STW暫停用戶線程。
- 并發(fā)標(biāo)記:該階段會(huì)不斷從灰色隊(duì)列中取出待處理的對象,找到其下一級引用對象并標(biāo)記為灰色存入到灰色隊(duì)列中,同時(shí)將自己設(shè)置為黑色,隨后不斷重復(fù)下一級入隊(duì)、本級染黑的這個(gè)步驟,直到灰色隊(duì)列為空為止。
- 最終標(biāo)記:將所有用戶線程暫停,修復(fù)并發(fā)標(biāo)記期間產(chǎn)生變動(dòng)的對象,完成最終標(biāo)記確認(rèn),總體耗時(shí)相對于并發(fā)標(biāo)記會(huì)短一些。
- 篩選回收:完成最終標(biāo)記之后,G1垃圾回收器會(huì)基于用戶給定的最大停頓時(shí)間內(nèi),找到回收性價(jià)比最高的region采用標(biāo)記復(fù)制法(將存活對象復(fù)制到空閑的region)完成垃圾回收。
需要注意的是在并發(fā)標(biāo)記階段會(huì)涉及一個(gè)多標(biāo)和漏標(biāo)的問題:
- 在用戶線程并發(fā)操作期間,原本標(biāo)記為黑(即存活的對象)的對象被解引用,導(dǎo)致本該被回收的對象還是黑色,那就是多標(biāo)的情況。
- 在并發(fā)標(biāo)記期間,原本未被引用的白色對象被其他對象持有,卻還是處于白色標(biāo)記狀態(tài)導(dǎo)致誤回收,那就是漏標(biāo)的情況。
相比之下多標(biāo)也就是一個(gè)浮動(dòng)垃圾的問題,但是漏標(biāo)就會(huì)很嚴(yán)重了,因?yàn)樗赡軙?huì)導(dǎo)致垃圾誤回收的情況。于是G1就提出了一個(gè)SATB快照技術(shù)(Snapshot At The Beginning)和寫前屏障來解決漏標(biāo)問題,對應(yīng)的執(zhí)行步驟為:
(1) 進(jìn)行標(biāo)記時(shí)記錄前,通過創(chuàng)建原始快照記錄當(dāng)前標(biāo)記對象存活情況。
(2) 基于上述快照,在此之后新創(chuàng)建的對象一律標(biāo)記為黑色,例如下圖新創(chuàng)建的obj-e:
- 并發(fā)期間,原本被標(biāo)記為白色的對象被其他對象持有,即出現(xiàn)對象賦值操作,例如下面的obj-e持有obj-d,G1垃圾回收器就則會(huì)通過寫前屏障技術(shù)將被其他引用持有的白色對象(參考本例中被obj-b持有的obj-d)放到一個(gè)SATB隊(duì)列中,注意這個(gè)隊(duì)列每個(gè)線程獨(dú)有,最終這些隊(duì)列的結(jié)果會(huì)匯總到全局的SATB隊(duì)列中:
- 最終標(biāo)記階段,進(jìn)行STW,全局SATB會(huì)將各個(gè)線程的SATB結(jié)果歸并收集,這些對象一律視為黑色不處理,通過這種快照技術(shù)保證解決了漏標(biāo)的問題,但還是會(huì)存在多標(biāo)的情況,所以G1回收器還是會(huì)存在一些浮動(dòng)垃圾。
四、垃圾回收器常見參數(shù)調(diào)優(yōu)技巧
1. 動(dòng)態(tài)調(diào)整新生代大小
日常進(jìn)行JVM參數(shù)配置會(huì)看到有些同學(xué)會(huì)通過-xmn來指定新生代堆內(nèi)存,相對來說這種做法存在因?yàn)槎褍?nèi)存預(yù)估失敗而導(dǎo)致的響應(yīng)時(shí)間激增的問題。例如,我們定死新生代堆內(nèi)存為128m,而Eden區(qū)默認(rèn)情況下占用80%差不多102M,默認(rèn)情況下Eden區(qū)達(dá)到60%時(shí)就會(huì)觸發(fā)minor GC,這也就意味著每當(dāng)Eden區(qū)達(dá)到60M左右時(shí)就會(huì)觸發(fā)新生代GC。
因?yàn)槎ㄋ懒诵律亩褍?nèi)存大小覆蓋了G1的自動(dòng)調(diào)節(jié),所以在流量激增的情況下,60M的堆內(nèi)存是很容易被打滿的,這種情況下就非??赡艹霈F(xiàn)頻繁新生代GC進(jìn)而導(dǎo)致響應(yīng)時(shí)間長:
所以我們建議通過-XX:G1NewSizePercent(新生代占用堆內(nèi)存的最小比,默認(rèn)為5%)以及-XX:G1MaxNewSizePercent(新生代占用堆內(nèi)存的最大比,默認(rèn)為60%),所以針對g1垃圾回收器,我們建議通過設(shè)置最大堆和新生代比例來避免新生代堆內(nèi)存分配不當(dāng)導(dǎo)致頻繁minor gc的開銷:
java -XX:+UseG1GC \
-XX:G1NewSizePercent=10 \
-XX:G1MaxNewSizePercent=50 \
-Xmx4g \
-jar myapp.jar
2. 調(diào)整并發(fā)回收stw耗時(shí)
上文已提及,G1垃圾回收器支持用戶給定的暫停時(shí)間內(nèi)完成mixed gc,為了保證進(jìn)行并發(fā)回收時(shí)停頓的時(shí)間盡可能少,保證進(jìn)程的并發(fā)性能表現(xiàn),我們建議暫停時(shí)間MaxGCPauseMillis上可以適當(dāng)調(diào)低一點(diǎn),以筆者為例,日常設(shè)置的MaxGCPauseMillis都是200ms:
# 啟動(dòng)應(yīng)用時(shí)添加JVM參數(shù)
java -XX:+UseG1GC \ # 啟用G1垃圾收集器
-Xmx4g \ # 堆內(nèi)存最大4G
-Xms4g \ # 堆內(nèi)存初始4G
-XX:MaxGCPauseMillis=100 \ # 設(shè)置最大GC停頓時(shí)間目標(biāo)
-jar your-app.jar
3. 提升降低并發(fā)回收間隔避免full gc
為避免并發(fā)回收不及時(shí),導(dǎo)致堆內(nèi)存被打滿觸發(fā)full gc導(dǎo)致程序并發(fā)性能下降,對于高并發(fā)讀請求的系統(tǒng),它們的堆內(nèi)存中老年代死亡率相對較高即短期老年代對象多,對此我們可以適當(dāng)調(diào)低-InitiatingHeapOccupancyPercent(觸發(fā)混合回收的閾值,默認(rèn)情況下的45即老年代內(nèi)存占用達(dá)到45%觸發(fā))盡早回收老年代對象來規(guī)避這個(gè)問題:
-XX:InitiatingHeapOccupancyPercent=35