本文已收錄至GitHub,推薦閱讀 ?? Java隨想錄
微信公眾號:Java隨想錄
原創(chuàng)不易,注重版權(quán)。轉(zhuǎn)載請注明原作者和原文鏈接
上篇文章我們聊了CMS,這篇就來好好嘮嘮G1。
CMS和G1可以說是一對歡喜冤家,面試問你CMS,總喜歡把G1拿進(jìn)來進(jìn)行比較。
G1在JDK7中加入JVM,在JDK9中成為了默認(rèn)的垃圾收集器,如果在JDK8中使用G1,我們可以使用參數(shù) -XX:+UseG1GC 來開啟。
G1和CMS相比有哪些優(yōu)缺點(diǎn)?G1為什么能夠建立可停頓的時(shí)間模型?
別著急,本篇文章告訴你答案。
G1,全名叫:Garbage First。是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果,開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式。
這句話啥意思?
在G1之前的垃圾回收器,如Parallel Scavenge、Parallel Old、CMS等,主要針對Java堆內(nèi)存中的特定部分(新生代或老年代)進(jìn)行操作。然而,G1將Java堆劃分為多個(gè)「小區(qū)域」,并根據(jù)每個(gè)區(qū)域中垃圾對象的數(shù)量和大小來優(yōu)先進(jìn)行垃圾回收。
稱之為「基于Region的內(nèi)存布局」。
另外設(shè)計(jì)者們設(shè)計(jì)G1的時(shí)候希望G1能夠建立起「停頓時(shí)間模型」,停頓時(shí)間模型的意思是能夠支持指定在一個(gè)長度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間大概率不超過N毫秒這樣的目標(biāo)。
所以我們能夠總結(jié)出G1身上的兩個(gè)標(biāo)簽:
- 基于Region的內(nèi)存布局
- 停頓時(shí)間模型
先來說說基于內(nèi)存布局是怎么個(gè)事兒。
基于Region的堆內(nèi)存布局
G1的基于Region的堆內(nèi)存布局,這是能夠建立起「停頓時(shí)間模型」的關(guān)鍵。
G1邏輯上分代,但是物理上不分代。
G1不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域,每一個(gè)區(qū)域稱之為「Region」。
每一個(gè)Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理。
好比角色扮演,不同的角色拿著不同的劇本。
G1可以通過參數(shù)控制新生代內(nèi)存的大?。?code>-XX:G1NewSizePercent(默認(rèn)等于5),-XX:G1MaxNewSizePercent
(默認(rèn)等于60)。
也就是說新生代大小默認(rèn)占整個(gè)堆內(nèi)存的 5% ~ 60%
。
G1收集器將整個(gè)Java堆劃分成約「2048個(gè)大小相同的獨(dú)立Region塊」,每個(gè)Region的大小可以通過參數(shù)-XX:G1HeapRegionSize
設(shè)定,取值范圍為1MB~32MB,且應(yīng)為2的N次冪。
可以簡單推算一下,G1能管理的最大內(nèi)存大約 32MB * 2048 = 64G左右。
Region中還有一類特殊的「Humongous區(qū)域」,專門用來存儲大對象,可以簡單理解為對應(yīng)著老年代。
G1認(rèn)為只要大小超過了一個(gè)Region容量一半的對象(即超過1.5個(gè)region)即可判定為大對象。
而對于那些超過了整個(gè)Region容量的超級大對象,將會被存放在N個(gè)連續(xù)的Humongous Region之中。
G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進(jìn)行看待。
分配大對象的時(shí)候,因?yàn)檎加每臻g太大,可能會過早發(fā)生GC停頓。G1在每次分配大對象的時(shí)候都會去檢查當(dāng)前堆內(nèi)存占用是否超過初始堆占用閾值IHOP(The Initiating Heap Occupancy Percent),缺省情況是Java堆內(nèi)存的45%。當(dāng)老年代的空間超過45%,G1會啟動一次混合周期。
可預(yù)測的停頓時(shí)間模型
基于Region的停頓時(shí)間模型是G1能夠建立可預(yù)測的停頓時(shí)間模型的前提。
G1將Region作為單次回收的最小單元,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍,這樣可以有計(jì)劃地避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。
G1收集器會去跟蹤各個(gè)Region里面的垃圾堆積的「價(jià)值」大小,價(jià)值即回收所獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值,然后在后臺維護(hù)一個(gè)優(yōu)先級列表。
每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間(使用參數(shù)-XX:MaxGCPauseMillis
指定,默認(rèn)值是200毫秒),優(yōu)先處理回收價(jià)值收益最大的那些Region,這也就是「Garbage First」名字的由來。
這種使用Region劃分內(nèi)存空間,以及具有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時(shí)間內(nèi)獲取盡可能高的收集效率。
所以說G1實(shí)現(xiàn)可預(yù)測的停頓時(shí)間模型的關(guān)鍵就是Region布局
和優(yōu)先級隊(duì)列
??雌饋砗孟馟1的實(shí)現(xiàn)也不復(fù)雜,但是其實(shí)有許多細(xì)節(jié)是需要考慮的。
跨Region引用對象
首先第一個(gè)問題:G1將Java堆分成多個(gè)獨(dú)立Region后,Region里面存在的跨Region引用對象如何解決?
本質(zhì)上還是我們之前提過的「跨代引用」問題,解決方案的思路我們已經(jīng)知道,使用「記憶集」。
G1的記憶集在存儲結(jié)構(gòu)的本質(zhì)上是一種「哈希表」,Key是別的Region的起始地址,Value是一個(gè)集合,里面存儲的元素是卡表的索引號。
使用記憶集固然沒啥毛病,但是麻煩的是,G1的堆內(nèi)存是以Region為基本回收單位的,所以它的每個(gè)Region都維護(hù)有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,并標(biāo)記這些指針分別在哪些卡頁的范圍之內(nèi)。
由于Region數(shù)量較多,每個(gè)Region都維護(hù)有自己的記憶集,光是存儲記憶集這塊就要占用相當(dāng)一部分內(nèi)存,G1比其他圾收集器有著更高的內(nèi)存占用負(fù)擔(dān)。根據(jù)經(jīng)驗(yàn),G1至少要耗費(fèi)大約相當(dāng)于Java堆容量10%至20%的額外內(nèi)存來維持收集器工作。
這可以說是G1的缺陷之一。
除了跨代引用外,對象引用關(guān)系改變,如何解決?
對象引用關(guān)系改變
解決的辦法我們之前在講「三色標(biāo)記算法」的時(shí)候提過,G1使用「原始快照」來解決這一問題。
垃圾收集對用戶線程的影響還體現(xiàn)在回收過程中新創(chuàng)建對象的內(nèi)存分配上,程序要繼續(xù)運(yùn)行就肯定會持續(xù)有新對象被創(chuàng)建。
G1為每一個(gè)Region設(shè)計(jì)了兩個(gè)名為「TAMS(Top at Mark Start)」的指針。
把Region中的一部分空間劃分出來用于并發(fā)回收過程中的新對象分配,并發(fā)回收時(shí)新分配的對象地址都必須要在這兩個(gè)指針位置以上。G1收集器默認(rèn)在這個(gè)地址以上的對象是被隱式標(biāo)記過的,即默認(rèn)它們是存活的,不納入回收范圍。
與CMS中的「Concurrent Mode Failure」失敗會導(dǎo)致Full GC類似,如果內(nèi)存回收的速度趕不上內(nèi)存分配的速度,G1收集器也要被迫凍結(jié)用戶線程執(zhí)行,導(dǎo)致Full GC而產(chǎn)生長時(shí)間Stop The World。
G1可以通過-XX:MaxGCPauseMillis
參數(shù)設(shè)置垃圾收集的最大停頓時(shí)間的JVM參數(shù),單位為毫秒。
在垃圾收集過程中,G1收集器會記錄每個(gè)Region的回收耗時(shí)、每個(gè)Region記憶集里的臟卡數(shù)量等各個(gè)可測量的步驟花費(fèi)的成本,并分析得出平均值、標(biāo)準(zhǔn)偏差、置信度等統(tǒng)計(jì)信息。
然后通過這些信息預(yù)測現(xiàn)在開始回收的話,由哪些Region組成回收集才可以在不超過期望停頓時(shí)間的約束下獲得最高的收益。
G1收集器會根據(jù)這個(gè)設(shè)定值進(jìn)行自我調(diào)整以盡量達(dá)到這個(gè)暫停時(shí)間目標(biāo)。例如,如果設(shè)定了-XX:MaxGCPauseMillis=200
,那么JVM會盡力保證大部分(但并非全部)的GC暫停時(shí)間不會超過200毫秒。
運(yùn)作過程
- 初始標(biāo)記(Initial Marking):僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發(fā)運(yùn)行時(shí),能正確地在可用的Region中分配新對象。這個(gè)階段需要停頓線程,但耗時(shí)很短,而且是借用進(jìn)行Minor GC的時(shí)候同步完成的,所以G1收集器在這個(gè)階段實(shí)際并沒有額外的停頓。
- 并發(fā)標(biāo)記(Concurrent Marking):從GC Root開始對堆中對象進(jìn)行可達(dá)性分析,遞歸掃描整個(gè)堆里的對象圖,找出要回收的對象,這階段耗時(shí)較長,但可與用戶程序并發(fā)執(zhí)行。當(dāng)對象圖掃描完成以后,還要重新處理SATB記錄下的在并發(fā)時(shí)有引用變動的對象。
- 最終標(biāo)記(Final Marking):對用戶線程做另一個(gè)短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的最后那少量的SATB記錄。
- 篩選回收(Live Data Counting and Evacuation):負(fù)責(zé)更新Region的統(tǒng)計(jì)數(shù)據(jù),對各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的停頓時(shí)間來制定回收計(jì)劃,可以自由選擇任意多個(gè)Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對象復(fù)制到空的Region中,再清理掉整個(gè)舊Region的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
從上述階段的描述可以看出,G1收集器除了并發(fā)標(biāo)記外,其余階段也是要完全暫停用戶線程的。
G1在邏輯上仍然采用了分代的思想,從整體來看是基于「標(biāo)記-整理」算法實(shí)現(xiàn)的收集器,但從局部(兩個(gè)Region之間)上看又是基于「標(biāo)記-復(fù)制」算法實(shí)現(xiàn)。
這時(shí)候有些點(diǎn)子王可能會想,如果我把-XX:MaxGCPauseMillis
,調(diào)的非常小,那是不是就回收的更快了?
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-ZmN6LAmN-1693277073940)(https://mmbiz.qpic.cn/mmbiz_jpg/jC8rtGdWScOt4Libmz1bw1WwRF0kfXiaCBqyBkHebh0n4oKickldRJRPAA60ibtlEJfYhrVaekBg3Y8dXrOuwWUeDA/640)]
G1默認(rèn)的停頓目標(biāo)為兩百毫秒,但如果我們把停頓時(shí)間調(diào)得非常低,譬如設(shè)置為二十毫秒,很可能出現(xiàn)的結(jié)果就是由于停頓目標(biāo)時(shí)間太短,導(dǎo)致每次選出來的回收集只占堆內(nèi)存很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導(dǎo)致垃圾慢慢堆積。
應(yīng)用運(yùn)行時(shí)間一長,最終占滿堆引發(fā)Full GC反而降低性能,所以通常把期望停頓時(shí)間設(shè)置為一兩百毫秒或者兩三百毫秒會是比較合理的。
CMS VS G1
相比CMS,G1的優(yōu)點(diǎn)有很多,較為明顯的優(yōu)點(diǎn)就是G1不會產(chǎn)生垃圾碎片。
But,G1相對于CMS仍然不是占全方位、壓倒性優(yōu)勢的,至少G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用(Footprint)還是程序運(yùn)行時(shí)的額外執(zhí)行負(fù)載(Overload)都要比CMS要高。
就內(nèi)存占用來說,雖然G1和CMS都使用卡表來處理跨代指針,但G1的每個(gè)Region都必須有一份卡表,這導(dǎo)致G1的記憶集可能會占整個(gè)堆容量的20%乃至更多的內(nèi)存空間,相比起來CMS的卡表就相當(dāng)簡單,全局只有一份。
在執(zhí)行負(fù)載的角度上,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護(hù)卡表;而G1除了使用寫后屏障來進(jìn)行同樣的卡表維護(hù)操作外,為了實(shí)現(xiàn)原始快照搜索(SATB)算法,還需要使用寫前屏障來跟蹤并發(fā)時(shí)的指針變化情況。
相比起增量更新算法,原始快照搜索能夠減少并發(fā)標(biāo)記和重新標(biāo)記階段的消耗,避免CMS那樣在最終標(biāo)記階段停頓時(shí)間過長的缺點(diǎn),但是在用戶程序運(yùn)行過程中確實(shí)會產(chǎn)生由跟蹤引用變化帶來的額外負(fù)擔(dān)。
由于G1對寫屏障的復(fù)雜操作要比CMS消耗更多的運(yùn)算資源,所以CMS的寫屏障實(shí)現(xiàn)是直接的同步操作,而G1就不得不將其實(shí)現(xiàn)為類似于消息隊(duì)列的結(jié)構(gòu),把寫前屏障和寫后屏障中要做的事情都放到隊(duì)列里,然后再異步處理。
目前在小內(nèi)存應(yīng)用上CMS的表現(xiàn)大概率仍然要會優(yōu)于G1,而在大內(nèi)存應(yīng)用上G1則大多能發(fā)揮其優(yōu)勢,這個(gè)優(yōu)劣勢的Java堆容量平衡點(diǎn)通常在6GB至8GB之間。
文章的最后放一組G1的常用參數(shù):
參數(shù) | 描述 |
---|---|
-XX:+UseG1GC | 手動指定使用G1收集器執(zhí)行內(nèi)存回收任務(wù)(JDK9后不用設(shè)置,默認(rèn)就是G1) |
-XX:G1HeapRegionSize | 設(shè)置每個(gè)Region的大小。值是2的冪,范圍是1MB到32MB之間,目標(biāo)是根據(jù)最小的Java堆大小劃分出約2048個(gè)區(qū)域。默認(rèn)是堆內(nèi)存的1/2000 |
-XX:MaxGCPauseMillis | 設(shè)置期望達(dá)到的最大GC停頓時(shí)間指標(biāo) |
-XX:InitiatingHeapOccupancyPercent | 簡稱為IHOP,設(shè)置觸發(fā)并發(fā)GC周期的Java堆占用率閾值。超過此值,就觸發(fā)GC。默認(rèn)值是45% |
-XX:+G1UseAdaptiveIHOP | 自動調(diào)整IHOP的指,JDK9之后可用 |
-XX:GCTimeRatio | 這個(gè)參數(shù)為0~100之間的整數(shù)(G1默認(rèn)是9),值為 n 則系統(tǒng)將花費(fèi)不超過 1/(1+n) 的時(shí)間用于垃圾收集。因此G1默認(rèn)最多 10% 的時(shí)間用于垃圾收集 |
最后吐槽一句,JVM真的很難,垃圾收集器的內(nèi)部原理實(shí)在太復(fù)雜,如果要深究需要長時(shí)間的積累。
當(dāng)然我們不是JVM的專業(yè)人員,不需要學(xué)的那么深入,這篇文章講到的內(nèi)容能基本應(yīng)付面試和工作場景了。
那本篇文章到這結(jié)束啦,我們下篇再見????。
感謝閱讀,如果本篇文章有任何錯(cuò)誤和建議,歡迎給我留言指正。
老鐵們,關(guān)注我的微信公眾號「Java 隨想錄」,專注分享Java技術(shù)干貨,文章持續(xù)更新,可以關(guān)注公眾號第一時(shí)間閱讀。文章來源:http://www.zghlxwxcb.cn/news/detail-679726.html
一起交流學(xué)習(xí),期待與你共同進(jìn)步!文章來源地址http://www.zghlxwxcb.cn/news/detail-679726.html
到了這里,關(guān)于深入解析G1垃圾回收器的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!