引言
? ?在《GC分代篇》中,我們?cè)鴮?duì)JVM中的分代GC收集器進(jìn)行了全面闡述引润,而在本章中重點(diǎn)則是對(duì)JDK后續(xù)新版本中研發(fā)推出的高性能收集器進(jìn)行深入剖析巩趁,但在開始前,先來看看JDK的發(fā)布記錄中關(guān)于GC體系的改變:
- 2018年9月:JDK11發(fā)布淳附,引入
Epsilon
垃圾回收器议慰,又被稱為"No-0p
(無操作) "回收器。同時(shí)奴曙,引入了可伸縮的低延遲垃圾回收器ZGC
(Experimental
)别凹。 - 2019年3月:JDK12發(fā)布,增強(qiáng)G1收集器洽糟,實(shí)現(xiàn)自動(dòng)返還未用堆內(nèi)存給操作系統(tǒng)。同時(shí)种蘸,引入了低停頓時(shí)間的收集器
ShenandoahGC
(Experimental
)蹋绽。 - 2019年9月:JDK13發(fā)布,增強(qiáng)ZGC收集器嘱丢,實(shí)現(xiàn)自動(dòng)返還未用堆內(nèi)存給操作系統(tǒng)。
- 2020年3月:JDK14發(fā)布祠饺,剔除了CMS收集器越驻,同時(shí)擴(kuò)展
ZGC
在macOS
和Windows
上的應(yīng)用,增強(qiáng)G1
支持NUMA
架構(gòu)吠裆。
? ?從如上JDK的發(fā)布日志中可以得出三款新的GC收集器伐谈,分別為:Epsilon、ZGC试疙、ShenandoahGC
诵棵,從此之后,JVM中的“GC家族”正式湊齊了十款收集器祝旷,如下圖:
? ?其中
Epsilon
主要是用于測(cè)試的無操作收集器履澳,如:性能測(cè)試、內(nèi)存壓力測(cè)試怀跛、VM接口測(cè)試等距贷。在程序啟動(dòng)時(shí)選擇裝載Epsilon收集器,這樣可以幫助我們過濾掉GC機(jī)制引起的性能假象吻谋。裝配該款GC收集器的JVM忠蝗,在運(yùn)行期間不會(huì)發(fā)生任何GC相關(guān)的操作,程序所分配的堆空間一旦用完漓拾,Java程序就會(huì)因OOM原因退出阁最。
因?yàn)镚1收集器是開啟了整堆回收的里程碑,所以在分代篇中并未對(duì)它進(jìn)行闡述骇两,在該篇中會(huì)從G1收集器出發(fā)速种,依次對(duì)ZGC、ShenandoahGC進(jìn)行全面剖析低千。
一配阵、開創(chuàng)GC“分區(qū)回收”新時(shí)代的里程碑 - G1
? ?在《分代篇》中談到:CMS收集器是JVM中開辟并發(fā)收集的里程碑,而本次的主角G1則是開創(chuàng)GC分區(qū)回收新時(shí)代的里程碑示血。在G1之前棋傍,所有的收集器都是按部就班的執(zhí)行“物理+邏輯分代”的收集原則,而在G1收集器中难审,開始正式將堆空間劃分為“物理分區(qū)不分代”的內(nèi)存結(jié)構(gòu)舍沙,從此拉開了JVM分區(qū)回收的序幕。
? ? G1全稱為Garbage-First Garbage Collector
(垃圾優(yōu)先收集器)剔宪,該款收集器在JDK1.7時(shí)被引入Java拂铡,在1.7之后壹无,我們可以通過參數(shù)-XX:+UseG1GC
裝配它。G1是一款專門針對(duì)于擁有多核處理器和大內(nèi)存的機(jī)器的收集器感帅,在滿足了GC響應(yīng)時(shí)間的延遲可控的情況下斗锭,也會(huì)盡可能提高的程序的吞吐量,官方推出該款收集器的目的在于:打算使用G1替換CMS收集器失球,并且讓G1擔(dān)當(dāng)起全功能收集器的重任和期望岖是,成為JVM中的第一款能夠駕馭一切的全功能垃圾收集器。
G1收集器具備如下特性:
- ①與CMS收集器一樣实苞,能夠與用戶線程同時(shí)執(zhí)行豺撑,完成并發(fā)收集。
- ②GC過程會(huì)有整理內(nèi)存的過程黔牵,不會(huì)產(chǎn)生內(nèi)存碎片聪轿,并且整理空閑內(nèi)存速度更快。
- ③GC發(fā)生時(shí)猾浦,停頓時(shí)間可控陆错,可以讓程序更大程度上追求低延遲。
- ④追求低延遲的同時(shí)金赦,盡可能會(huì)保證高吞吐量音瓷。
- ⑤對(duì)于堆的未使用內(nèi)存可以返還給操作系統(tǒng)。
接下來我們首先以G1的內(nèi)存劃分為起點(diǎn)出發(fā)夹抗,對(duì)該款收集器進(jìn)行全面剖析绳慎。
1.1、G1收集器的堆空間內(nèi)存劃分
在G1收集器之前漠烧,Java的堆空間大致都長(zhǎng)這個(gè)樣子:
? ?到了JDK1.9時(shí)杏愤,堆空間慢慢的開始了劃時(shí)代的改變,在此之前沽甥,堆空間的布局都是采用分代存儲(chǔ)的方式,無論從邏輯上還是從物理內(nèi)存上乏奥,都是分代的摆舟。但是到了Java9的時(shí)候,因?yàn)槟J(rèn)GC器改為了G1邓了,所以堆中的內(nèi)存區(qū)域被劃為了一個(gè)個(gè)的
Region
區(qū)恨诱。
? ?每個(gè)分區(qū)都可能是年輕代也可能是老年代,但是在同一時(shí)刻只能屬于某個(gè)代骗炉。在運(yùn)行時(shí)照宝,每個(gè)分區(qū)都會(huì)被打上唯一的分區(qū)標(biāo)識(shí)。
? ?不過在G1收集器中句葵,年輕代Eden
區(qū)厕鹃、幸存區(qū)Survivor
兢仰、老年代Old
區(qū)這些概念依舊還存在,但卻成為了邏輯上的概念剂碴,這樣做的好處在于:也可以復(fù)用之前分代框架的邏輯把将,同時(shí)也滿足了Java對(duì)象朝生夕死的特性。
不過G1收集器雖然在邏輯上存在分代的概念忆矛,但不再是物理隔閡了察蹲,也就是指在物理內(nèi)存上是不分代的,內(nèi)存空間會(huì)被劃分為一個(gè)個(gè)的
Region
區(qū)催训,這樣做的好處在于:JVM不需要再為堆空間分配連續(xù)的內(nèi)存洽议,堆空間可以是不連續(xù)物理內(nèi)存來組成Region
的集合。
同時(shí)也帶來了額外的好處:有的Region
區(qū)內(nèi)垃圾對(duì)象特別多漫拭,有的分區(qū)內(nèi)垃圾對(duì)象很少亚兄,G1可以優(yōu)先回收垃圾對(duì)象特別多的Region
區(qū),這樣可以花費(fèi)較少的時(shí)間來回收垃圾嫂侍,這也就是G1名字的由來儿捧,即垃圾優(yōu)先收集器。
在運(yùn)行時(shí)挑宠,G1收集器會(huì)將堆空間變?yōu)槿缦陆Y(jié)構(gòu):
? ?G1將Java堆劃分為多個(gè)大小相等的獨(dú)立的
Region
區(qū)域菲盾,不過在HotSpot
的源碼TARGET_REGION_NUMBER
定義了Region
區(qū)的數(shù)量限制為2048
個(gè)(實(shí)際上允許超過這個(gè)值,但是超過這個(gè)數(shù)量后各淀,堆空間會(huì)變的難以管理)懒鉴。
一般
Region
區(qū)的大小等于堆空間的總大小除以2048,比如目前的堆空間總大小為8GB碎浇,就是8192MB/2048=4MB
临谱,那么最終每個(gè)Region
區(qū)的大小為4MB
,當(dāng)然也可以用參數(shù)-XX:G1HeapRegionSize
強(qiáng)制指定每個(gè)Region
區(qū)的大小奴璃,但是不推薦悉默,畢竟默認(rèn)的計(jì)算方式計(jì)算出的大小是最適合管理堆空間的。
對(duì)于這塊的邏輯苟穆,在HotSpot源碼的/share/vm/gc_implementation/g1/heapRegion.cpp
文件中定義了具體實(shí)現(xiàn)抄课,具體可以參考其內(nèi)部的HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size)
函數(shù)。
? ?默認(rèn)新生代對(duì)堆內(nèi)存的初始占比是5%雳旅,如果堆大小為8GB跟磨,那么年輕代占據(jù)400MB
左右的內(nèi)存,對(duì)應(yīng)大概是200個(gè)Region
區(qū)攒盈,可以通過-XX:G1NewSizePercent
設(shè)置新生代初始占比抵拘。
? ?在Java程序運(yùn)行中,JVM會(huì)不停的給新生代增加更多的Region
區(qū)型豁,但是最多新生代的占比不會(huì)超過堆空間總大小的60%僵蛛,可以通過-XX:G1MaxNewSizePercent
調(diào)整(也不推薦尚蝌,如果超過這個(gè)比例,年老代的空間會(huì)變的很小墩瞳,容易觸發(fā)全局GC)驼壶。新生代中的Eden
區(qū)和Survivor
區(qū)對(duì)應(yīng)的Region
區(qū)比例也跟之前一樣,默認(rèn)8:1:1喉酌,假設(shè)新生代現(xiàn)在有400個(gè)Region
热凹,那么整個(gè)新生代的占比則為Eden=320,S0/From=40,S1/To=40
。
? ?G1中的年老代晉升條件和之前的無差泪电,達(dá)到年齡閾值的對(duì)象會(huì)被轉(zhuǎn)入年老代的Region
區(qū)中般妙,不同的是對(duì)于大對(duì)象的分配,在G1中不會(huì)讓大對(duì)象進(jìn)入年老代相速,在G1中由專門存放大對(duì)象的Region
區(qū)叫做Humongous
區(qū)碟渺,如果在分配對(duì)象時(shí),判定出一個(gè)對(duì)象屬于大對(duì)象突诬,那么則會(huì)直接將其放入Humongous
區(qū)存儲(chǔ)苫拍。
在G1中,判定一個(gè)對(duì)象是否為大對(duì)象的方式為:對(duì)象大小是否超過單個(gè)普通
Region
區(qū)的50%旺隙,如果超過則代表當(dāng)前對(duì)象為大對(duì)象绒极,那么該對(duì)象會(huì)被直接放入Humongous
區(qū)。比如:目前是8GB的堆空間蔬捷,每個(gè)Region
區(qū)的大小為4MB
垄提,當(dāng)一個(gè)對(duì)象大小超過2MB
時(shí)則會(huì)被判定為屬于大對(duì)象。如果程序運(yùn)行過程中出現(xiàn)一個(gè)“巨型對(duì)象”周拐,當(dāng)一個(gè)Humongous
區(qū)存不下時(shí)铡俐,可能會(huì)橫跨多個(gè)Region
區(qū)存儲(chǔ)它。
? ?Humongous
區(qū)存在的意義:可以避免一些“短命”的巨型對(duì)象直接進(jìn)入年老代妥粟,節(jié)約年老代的內(nèi)存空間审丘,可以有效避免年老代因空間不足時(shí)的GC開銷。
? ?當(dāng)堆空間發(fā)生全局GC(FullGC
)時(shí)勾给,除開回收新生代和年老代之外滩报,也會(huì)對(duì)Humongous
區(qū)進(jìn)行回收。
其實(shí)在《JVM成神路第四章:JVM運(yùn)行時(shí)內(nèi)存劃分》中也提及過:邏輯分代+物理分區(qū)的結(jié)構(gòu)是堆空間最佳的方案锦秒,所以G1的這種結(jié)構(gòu)也是最理想的結(jié)構(gòu)露泊,但后續(xù)的ZGC喉镰、ShenandoahGC收集器中旅择,因?yàn)閷?shí)現(xiàn)方面的某些原因,導(dǎo)致最終無法采用這種結(jié)構(gòu)侣姆。
1.2生真、G1收集器GC類型
G1中主要存在YoungGC沉噩、MixedGC
以及FullGC
三種GC類型,這三種GC類型分別會(huì)在不同情景下被觸發(fā)柱蟀。
1.2.1川蒙、YoungGC
? ?前面提及過,G1對(duì)于整個(gè)堆空間所有的Region
區(qū)不會(huì)在一開始就全部分配完长已,無論是新生代畜眨、幸存區(qū)以及年老代在最開始都是會(huì)有初始數(shù)量的,在程序運(yùn)行過程中會(huì)根據(jù)需求不斷增加每個(gè)分代區(qū)域的Region
數(shù)量术瓮。
? ?所以YoungGC
并非說Eden
區(qū)放滿了就會(huì)立馬被觸發(fā)康聂,在G1中,當(dāng)新生代區(qū)域被用完時(shí)胞四,G1首先會(huì)大概計(jì)算一下回收當(dāng)前的新生代空間需要花費(fèi)多少時(shí)間恬汁,如果回收時(shí)間遠(yuǎn)遠(yuǎn)小于參數(shù)-XX:MaxGCPauseMills
設(shè)定的值,那么不會(huì)觸發(fā)YoungGC
辜伟,而是會(huì)繼續(xù)為新生代增加新的Region
區(qū)用于存放新分配的對(duì)象實(shí)例氓侧。直至某次Eden
區(qū)空間再次被放滿并經(jīng)過計(jì)算后,此次回收的耗時(shí)接近-XX:MaxGCPauseMills
參數(shù)設(shè)定的值导狡,那么才會(huì)觸發(fā)YoungGC
约巷。
? ?G1收集器中的新生代收集,依舊保留了分代收集器的特性烘豌,當(dāng)YoungGC
被觸發(fā)時(shí)载庭,首先會(huì)將目標(biāo)Region
區(qū)中的存活對(duì)象移動(dòng)至幸存區(qū)空間(被打著Survivor-from
區(qū)標(biāo)志的Region
)。同時(shí)達(dá)到晉升年齡標(biāo)準(zhǔn)的對(duì)象也會(huì)被移入至年老代Region
中存儲(chǔ)廊佩。
值得注意的是:G1收集器在發(fā)生
YoungGC
時(shí)囚聚,復(fù)制移動(dòng)對(duì)象時(shí)是采用的多線程并行復(fù)制,以此來換取更優(yōu)異的GC性能标锄。
用戶如若未曾顯式通過-XX:MaxGCPauseMills
參數(shù)設(shè)定GC預(yù)期回收停頓時(shí)間值顽铸,那么G1默認(rèn)為200ms
。
1.2.2料皇、MixedGC
? ?MixedGC
翻譯過來的意思為混合型GC谓松,而并非是指FullGC
。當(dāng)整個(gè)堆中年老代的區(qū)域占有率達(dá)到參數(shù)-XX:InitiatingHeapOccupancyPercent
設(shè)定的值后觸發(fā)MixedGC
践剂,發(fā)生該類型GC后鬼譬,會(huì)回收所有新生代Region
區(qū)、部分年老代Region
區(qū)(會(huì)根據(jù)期望的GC停頓時(shí)間選擇合適的年老代Region
區(qū)優(yōu)先回收)以及大對(duì)象Humongous
區(qū)逊脯。
? ?正常情況下优质,G1垃圾收集時(shí)會(huì)先發(fā)生
MixedGC
,主要采用復(fù)制算法,在GC時(shí)先將要回收的Region
區(qū)中存活的對(duì)象拷貝至別的Region
區(qū)內(nèi)巩螃,拷貝過程中演怎,如果發(fā)現(xiàn)沒有足夠多的空閑Region
區(qū)承載拷貝對(duì)象,此時(shí)就會(huì)觸發(fā)一次Full GC
避乏。
1.2.3爷耀、FullGC
? ?當(dāng)整個(gè)堆空間中的空閑Region
不足以支撐拷貝對(duì)象或由于元數(shù)據(jù)空間滿了等原因觸發(fā),在發(fā)生FullGC
時(shí)拍皮,G1首先會(huì)停止系統(tǒng)所有用戶線程歹叮,然后采用單線程進(jìn)行標(biāo)記、清理和壓縮整理內(nèi)存铆帽,以便于清理出足夠多的空閑Region
來供下一次MixedGC
使用盗胀。但該過程是單線程串行收集的,因此這個(gè)過程非常耗時(shí)的(ShenandoahGC
中采用了多線程并行收集)锄贼。
其實(shí)G1收集器中并沒有FullGC票灰,,G1中的FullGC是采用serial old FullGC宅荤。因?yàn)镚1在設(shè)計(jì)時(shí)的初衷就是要避免發(fā)生FullGC屑迂,如果上述兩種GC發(fā)生后還是無法使得程序恢復(fù)正常執(zhí)行,最終就會(huì)觸發(fā)SerialOld收集器的FullGC冯键。
1.3惹盼、G1收集器垃圾回收過程
G1收集器一般在發(fā)生GC時(shí)執(zhí)行過程大致會(huì)分為四個(gè)步驟(主要指MixedGC
):
- ①初始標(biāo)記(
InitialMark
):先觸發(fā)STW
,然后使用單條GC線程快速標(biāo)記GCRoots
直連的對(duì)象惫确。 - ②并發(fā)標(biāo)記(
ConcurrentMarking
):與CMS的并發(fā)標(biāo)記過程一致手报,采用多條GC線程與用戶線程共同執(zhí)行,根據(jù)Root
根節(jié)點(diǎn)標(biāo)記所有對(duì)象改化。 - ③最終標(biāo)記(
Remark
):同CMS的重新標(biāo)記階段掩蛤,主要是為了糾正并發(fā)標(biāo)記階段因用戶操作導(dǎo)致的錯(cuò)標(biāo)、誤標(biāo)陈肛、漏標(biāo)對(duì)象揍鸟。 - ④篩選回收(
Cleanup
):先對(duì)各個(gè)Region
區(qū)的回收價(jià)值和成本進(jìn)行排序,找出「回收價(jià)值最大」的Region
優(yōu)先回收句旱。
G1收集器正是由于「篩選回收」階段的存在阳藻,所以才得以冠名「垃圾優(yōu)先收集器」。在該階段中谈撒,對(duì)各個(gè)
Region
區(qū)排序后腥泥,G1會(huì)根據(jù)用戶指定的期望停頓時(shí)間(即-XX:MaxGCPauseMillis
參數(shù)設(shè)定的值)選擇「價(jià)值最大且最符合用戶預(yù)期」的Region
區(qū)進(jìn)行回收,舉個(gè)例子:
假設(shè)此時(shí)年老代空間共有800
個(gè)Region
區(qū)啃匿,并且都滿了蛔外,所以此刻會(huì)觸發(fā)GC。但根據(jù)GC的預(yù)期停頓時(shí)間值,本次GC只能允許停頓200ms
冒萄,而G1經(jīng)過前面的成本計(jì)算后,大致可推斷出:本次GC回收600
個(gè)Region
區(qū)恰好停頓時(shí)間可控制在200ms
左右橙数,那么最終就會(huì)以「回收600
個(gè)Region
區(qū)」為基準(zhǔn)觸發(fā)GC尊流,這樣則能盡量確保GC導(dǎo)致的停頓時(shí)間可以被控制在我們指定的范圍之內(nèi)。
? ?不過值得注意的是:篩選回收階段在G1收集器中是會(huì)停止所有用戶線程后灯帮,采用多線程并行回收的崖技。但實(shí)際上這個(gè)過程中可以與用戶線程一起執(zhí)行做到并發(fā)收集的,但因?yàn)镚1只回收一部分Region
區(qū)钟哥,停頓時(shí)間是可控的迎献,因此停止用戶線程后回收效率會(huì)大幅度提高。同時(shí)腻贰,假設(shè)實(shí)現(xiàn)并發(fā)回收吁恍,則又需要考慮用戶線程執(zhí)行帶來的一些問題,所以綜合考慮播演,G1中回收階段采用了發(fā)生STW
方案完成(在后續(xù)的ZGC冀瓦、ShenandoahGC
收集器中實(shí)現(xiàn)了并發(fā)回收),G1收集過程如下:
? ?在G1中不管是新生代還是年老代写烤,回收算法都是采用復(fù)制算法翼闽,在GC發(fā)生時(shí)都會(huì)將一個(gè)
Region
區(qū)中存活的對(duì)象復(fù)制到另外一個(gè)Region
區(qū)內(nèi)。同比之前的CMS收集器采用的標(biāo)-清算法而言洲炊,這種方式不會(huì)造成內(nèi)存碎片感局,因此也不需要花費(fèi)額外的成本整理內(nèi)存。
但自
G1
開始暂衡,包括之后的ZGC询微、ShenandoahGC
收集器,從每個(gè)Region
區(qū)角度看來是采用的復(fù)制算法狂巢,但從堆空間整體看來拓提,則是采用了標(biāo)-整算法,這也是所謂的“局部復(fù)制隧膘,全局標(biāo)-整”代态。
這兩種算法無論是那種都不會(huì)造成內(nèi)存碎片產(chǎn)生,帶來的好處是:在為大對(duì)象進(jìn)行內(nèi)存分配時(shí)疹吃,不會(huì)因?yàn)檎也坏竭B續(xù)的內(nèi)存空間提前觸發(fā)下一次GC蹦疑,有利于程序長(zhǎng)期運(yùn)行,尤其是在大內(nèi)存情況下的堆空間萨驶,帶來的優(yōu)勢(shì)額外明顯歉摧。
不過注意:在內(nèi)存較小的堆空間情況下,CMS的表現(xiàn)會(huì)優(yōu)于G1收集器,平衡點(diǎn)在6~8GB
左右叁温。
1.4再悼、G1中三色標(biāo)記-漏標(biāo)問題解決方案剖析
? ?在《GC分代篇》中曾提及過:CMS收集器拉開了并發(fā)收集的新序幕,而并發(fā)收集的核心在于三色標(biāo)記算法膝但,但三色標(biāo)記又注定著會(huì)出現(xiàn)漏標(biāo)問題冲九,所以接下來探討一下G1收集器中解決三色算法漏標(biāo)問題的手段:STAB + 寫屏障。
對(duì)象的讀寫屏障的具體實(shí)現(xiàn)位于HotSpot源碼的:
hotspot/src/share/vm/oops/oop.inline.hpp
文件跟束,其內(nèi)部是通過內(nèi)聯(lián)方法實(shí)現(xiàn)(HotSpot虛擬機(jī)中寫屏障的實(shí)現(xiàn)有好幾個(gè)版本)莺奸。
1.4.1、STAB解決新分配對(duì)象的漏標(biāo)問題
? ?STAB全稱為snapshot-at-the-beginning
冀宴,其存在的意義是為了維護(hù)G1收集器GC-并發(fā)收集的正確性灭贷。GC的正確性是保證存活的對(duì)象不被回收,簡(jiǎn)單點(diǎn)來說就是保證回收的都是垃圾略贮。
如果是獨(dú)占式收集甚疟,也就是發(fā)生STW后串行回收的方式,那GC時(shí)能夠確保100%的正確性逃延,但如若收集過程是與用戶線程并發(fā)執(zhí)行的古拴,GC線程一邊標(biāo)記,用戶線程一邊執(zhí)行真友,因而堆中的對(duì)象引用會(huì)存在變更黄痪,出現(xiàn)不穩(wěn)定因素,最終導(dǎo)致標(biāo)記的正確性無法得到保障盔然。而為了解決該問題桅打,在G1收集器中則引入了STAB機(jī)制。
? ?STAB機(jī)制中愈案,會(huì)在GC開始標(biāo)記前通過RootTracing
生成的一張堆空間存活對(duì)象快照挺尾,在并發(fā)標(biāo)記時(shí),所有快照中當(dāng)時(shí)存活的對(duì)象就會(huì)認(rèn)為是存活的站绪,標(biāo)記過程中新分配的對(duì)象也會(huì)被標(biāo)記為存活對(duì)象遭铺,不會(huì)被回收,G1通過這種方式則可確保新分配對(duì)象的GC正確性恢准。不過在理解STAB具體操作前魂挂,先來看看Region
區(qū)結(jié)構(gòu):
在G1劃分的堆空間中廓鞠,每個(gè)
Region
區(qū)都包含了五個(gè)指針沪猴,分別為:bottom铅协、previous TAMS垄开、next TAMS、top
以及end
诊沪,如下圖:
? ?五個(gè)指針釋義如下:
- 兩個(gè)
TAMS(top-at-mark-start)
指針贸毕,用于記錄前后兩次發(fā)生并發(fā)標(biāo)記時(shí)的位置忆谓。 -
bottom
指針:當(dāng)前Region
區(qū)分配對(duì)象的起始位置。 -
top
指針:Region
區(qū)目前已用空間的位置秋泳。 -
end
指針:當(dāng)前Region
區(qū)能夠分配的最大位置潦闲。
如若感覺對(duì)于這幾根指針不理解,那么你可以將
Region
區(qū)想象成一個(gè)透明的玻璃杯迫皱,bottom
指針指向杯底位置歉闰、top
指針指向杯中水位的那條線、end
指針則是杯口/杯頂舍杜,也就是目前玻璃杯能夠裝水的最大容量。
并發(fā)標(biāo)記發(fā)生后赵辕,STAB具體過程如下:
- ①第N次GC被觸發(fā)既绩,首先會(huì)將
Region
區(qū)的top
指針賦值給nextTAMS
,在「并發(fā)標(biāo)記期間」新分配的對(duì)象都在nextTAMS ~ top
指針之間还惠,STAB機(jī)制能夠確保這部分對(duì)象是絕對(duì)會(huì)被標(biāo)記饲握,默認(rèn)為存活的。 - ②當(dāng)「并發(fā)標(biāo)記」即將結(jié)束時(shí)蚕键,會(huì)將
nextTAMS
指針賦值給preTAMS
救欧,STAB機(jī)制緊接著會(huì)為bottom ~ preTAMS
之間的對(duì)象生成一個(gè)快照文件(BitMap
結(jié)構(gòu)),所有垃圾對(duì)象可通過快照文件識(shí)別出來锣光。 - ③后續(xù)每次GC不斷重復(fù)如上步驟笆怠。
- 如下示意圖中演示了兩輪GC并發(fā)標(biāo)記過程:
- ①階段是初始標(biāo)記階段,發(fā)生STW誊爹,將目標(biāo)
Region
區(qū)的Top
賦值給nextTAMS
蹬刷。 - ①~②階段是并發(fā)標(biāo)記階段,GC線程與用戶線程一同執(zhí)行频丘,并發(fā)標(biāo)記區(qū)內(nèi)所有存活對(duì)象办成。
- ②階段中,
nextTAMS ~ top
指針之間是新分配的對(duì)象搂漠,這些對(duì)象被稱為「隱式對(duì)象」迂卢,同時(shí)會(huì)使用一個(gè)BitMap
開始記錄「并發(fā)標(biāo)記」期間標(biāo)記的對(duì)象地址。 - ③階段則是清除回收階段桐汤,將
nextTAMS
賦值給preTAMS
(包括BitMap
中的標(biāo)記數(shù)據(jù)也會(huì)一同賦值給PrevBitMap
)而克,然后清理bottom ~ preTAMS
之間的所有垃圾對(duì)象。 - 對(duì)于并發(fā)標(biāo)記階段產(chǎn)生的「隱式對(duì)象」會(huì)在下次GC回收時(shí)再清除怔毛,如上圖中的階段⑥拍摇,會(huì)清除第一次GC-并發(fā)標(biāo)記時(shí)產(chǎn)生的新對(duì)象。這也是STAB機(jī)制存在的弊端馆截,會(huì)在一定程度上造成浮動(dòng)垃圾出現(xiàn)充活。
最終G1收集器中蜂莉,通過如上機(jī)制確保了并發(fā)標(biāo)記過程中,新對(duì)象不會(huì)漏標(biāo)(因?yàn)閴焊鶝]標(biāo)記新產(chǎn)生的對(duì)象混卵,直接默認(rèn)為所有新對(duì)象都是活的映穗,至于新對(duì)象到底是死是活,這件事情則留給下次GC來處理)幕随。
1.4.2蚁滋、STAB+寫屏障解決引用更改對(duì)象的漏標(biāo)問題
? ?對(duì)于新分配的對(duì)象漏標(biāo)問題在前面已經(jīng)闡述過了,那么G1收集器又是如何解決「并發(fā)標(biāo)記」過程中赘淮,原有對(duì)象引用發(fā)生更改導(dǎo)致的漏標(biāo)問題呢辕录?引用分代篇中的片段,如下:
①一條用戶線程在執(zhí)行過程中梢卸,斷開了一個(gè)未標(biāo)記的白色對(duì)象連接走诞,然后該對(duì)象又被一個(gè)已經(jīng)標(biāo)記成黑色的對(duì)象建立起了引用連接。如下圖:
白色對(duì)象斷開了左側(cè)灰色對(duì)象的引用蛤高,又與右側(cè)的黑色對(duì)象建立了新的引用關(guān)系蚣旱。
②一條用戶線程在執(zhí)行過程中,正好在GC線程標(biāo)記時(shí)戴陡,將一個(gè)灰色對(duì)象與一個(gè)未標(biāo)記的白色對(duì)象之間的引用連接斷開了塞绿,然后當(dāng)GC標(biāo)記完成這個(gè)灰色對(duì)象,將其標(biāo)記為黑色后恤批,之前斷開的白色對(duì)象又重新與之建立起了引用關(guān)系异吻。如下圖:
GC標(biāo)記前,白色對(duì)象斷開了與灰色對(duì)象的引用喜庞,四秒鐘之后GC標(biāo)記灰色對(duì)象完成涧黄,而此時(shí)恰巧白色對(duì)象又重新與標(biāo)記結(jié)束后成為黑色的對(duì)象重新建立了引用關(guān)系。
? ?而當(dāng)出現(xiàn)這兩種情況時(shí)赋荆,因?yàn)橹匦陆⒁玫陌咨珜?duì)象“父節(jié)點(diǎn)”已經(jīng)被標(biāo)記黑色了笋妥,所以GC線程不會(huì)再次標(biāo)記該對(duì)象以及其成員對(duì)象,所以這些白色對(duì)象會(huì)被一直停留在白色集合中窄潭。最終導(dǎo)致的結(jié)果就是這些依舊存在引用的存活對(duì)象會(huì)被“誤判”為垃圾對(duì)象清除掉春宣。而這種情況會(huì)直接影響到應(yīng)用程序的正確性,是不可接受的嫉你。
先來思考一下引起漏標(biāo)問題的原因:
條件一:灰色對(duì)象斷開了與白色對(duì)象的引用(直接引用或間接引用都可)月帝。
條件二:已經(jīng)標(biāo)為黑色的對(duì)象重新與白色對(duì)象建立了引用關(guān)系。
只有當(dāng)一個(gè)對(duì)象同時(shí)滿足了如上兩個(gè)條件時(shí)才可發(fā)生漏標(biāo)問題幽污。
上個(gè)簡(jiǎn)單的代碼案例理解一下:
Object X = obj.fieldX; // 獲取obj.fieldX成員對(duì)象
obj.fieldX = null; // 將原本obj.fieldX的引用斷開
objA.fieldX = X; // 將斷開引用的X白色對(duì)象與黑色對(duì)象objA建立引用
? ?從如上代碼角度來看嚷辅,假設(shè)obj
是一個(gè)灰色對(duì)象,此時(shí)先獲取它的成員fieldX
并將其賦值給變量X
距误,讓其堆中實(shí)例與變量X
保持著引用關(guān)系簸搞。緊接著再將obj.fieldX
置空扁位,斷開與obj
對(duì)象的引用關(guān)系,最后再與黑色對(duì)象objA
建立起引用關(guān)系趁俊,最終關(guān)系如下:
灰色對(duì)象
obj
域仇,白色對(duì)象obj.fieldX/X
,黑色對(duì)象objA
寺擂。
白色對(duì)象X
在GC機(jī)制標(biāo)記灰色對(duì)象obj
成員屬性之前暇务,與灰色對(duì)象斷開了引用,然后又“勾搭”上了黑色對(duì)象objA
怔软,此刻白色對(duì)象X
就會(huì)被永遠(yuǎn)停留在白色集合中垦细,直至清除階段到來,被“誤判”為垃圾回收掉挡逼。
? ?在CMS中為解決該問題的手段為:寫后屏障+增量更新括改,采用了寫后屏障記錄了更改引用的對(duì)象,然后通過溯源對(duì)發(fā)生改動(dòng)的節(jié)點(diǎn)進(jìn)行了重新掃描挚瘟。而G1中則是通過STAB+寫前屏障解決該問題叹谁,如下:
// HotSpot中對(duì)象字段賦值邏輯
void oop_field_store(oop* field, oop new_value) {
*field = new_value; // 賦值操作:新值替換老值
}
// HotSpot中對(duì)象字段寫屏障(最容易理解的實(shí)現(xiàn)版本)
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 寫前屏障
*field = new_value; // 賦值操作:新值替換老值
post_write_barrier(field, value); // 寫后屏障
}
G1收集器會(huì)通過寫前屏障饲梭,在引用被更改前先記錄一下原本的引用信息乘盖,如下:
void pre_write_barrier(oop* field) {
// 處于GC并發(fā)標(biāo)記階段且該對(duì)象沒有被標(biāo)記(訪問)過
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value = *field; // 獲取舊值
remark_set.add(old_value); // 記錄 原來的引用對(duì)象
}
}
? ?G1中的這種做法思路為:保留GC開始時(shí)的對(duì)象圖關(guān)系,即原始快照(SATB:Snapshot At The Beginning
)憔涉,并發(fā)標(biāo)記過程會(huì)以最初的對(duì)象圖關(guān)系進(jìn)行訪問订框,就算并發(fā)標(biāo)記過程中某個(gè)對(duì)象的引用信息發(fā)生了改變,G1會(huì)通過寫前屏障記錄原有的對(duì)象引用關(guān)系兜叨,依舊會(huì)按照最初的對(duì)象圖快照進(jìn)行標(biāo)記穿扳。
G1處理該問題時(shí)的思路非常簡(jiǎn)單,總而言之一句話:并發(fā)標(biāo)記過程中国旷,我不管你的引用關(guān)系怎么改變矛物,我反正就跟著最開始的對(duì)象圖關(guān)系進(jìn)行標(biāo)記。
值得額外注意的是:GC開始前的快照是個(gè)邏輯上的概念跪但,其實(shí)本質(zhì)上G1是一直通過寫前屏障維護(hù)著該快照履羞,從而達(dá)到依據(jù)「原始快照」進(jìn)行標(biāo)記的目標(biāo)。
G1是通過破壞「條件一:灰色對(duì)象斷開了與白色對(duì)象的引用屡久∫涫祝」解決了漏標(biāo)問題。
? ?上述過程中的源碼并非G1的真正實(shí)現(xiàn)被环,僅是偽代碼邏輯便于各位理解糙及,真正的實(shí)現(xiàn)位于G1SATBCardTableModRefBS::enqueue(oop pre_val)
函數(shù)中。其實(shí)在G1中引用發(fā)生更改時(shí)筛欢,會(huì)把原引用保存到satb_mark_queue
中浸锨,每條線程都自帶一個(gè)satb_mark_queue
唇聘。在下一次的并發(fā)標(biāo)記階段,會(huì)依次處理satb_mark_queue
中的對(duì)象揣钦,確保這部分對(duì)象在本輪GC中是存活的雳灾。
- 這樣STAB就保證了真正存活的對(duì)象不會(huì)被GC誤回收,但同時(shí)也造成了某些應(yīng)該被回收的垃圾對(duì)象逃過了GC冯凹,從而導(dǎo)致產(chǎn)生許多額外的浮動(dòng)垃圾(
float garbage
)谎亩。
1.5、G1中解決跨代引用手段 - RSet
? ?在分代篇中曾談到過:CMS以及之前的大部分的分代收集器為了解決跨代引用問題宇姚,實(shí)現(xiàn)記憶集時(shí)匈庭,都采用的為卡表CardTable
的結(jié)構(gòu),而在G1中則實(shí)現(xiàn)了一種新的數(shù)據(jù)結(jié)構(gòu):Remembered Set
浑劳,簡(jiǎn)稱為RSet
阱持,在有些地方也被稱為“雙向卡表”。
? ?在每個(gè)Region
區(qū)中都存在一個(gè)RSet
魔熏,其中記錄了其他Region
中的對(duì)象引用當(dāng)前Region
中對(duì)象的關(guān)系衷咽,也就是記錄著“誰引用了我的對(duì)象”,屬于points-into
結(jié)構(gòu)蒜绽。而之前的卡表則是屬于points-out
結(jié)構(gòu)镶骗,記錄著“我引用了誰的對(duì)象”,在卡表中存在多個(gè)卡頁躲雅,每個(gè)卡頁記錄著一定范圍(512KB
)的堆鼎姊。
RSet
也好,CardTable
也罷相赁,其實(shí)都是記憶集的一種具體實(shí)現(xiàn)相寇,你也可以將RSet
理解成一種CardTable
的進(jìn)階實(shí)現(xiàn)方式。G1中的RSet
本質(zhì)上就是一個(gè)哈希表結(jié)構(gòu)(HashTable
)钮科,Key
為其他引用當(dāng)前區(qū)內(nèi)對(duì)象的Region
起始地址唤衫,Value
則是一個(gè)集合,里面的元素為其他Region
中每個(gè)引用當(dāng)前區(qū)內(nèi)對(duì)象的地址绵脯。
在虛擬機(jī)運(yùn)行期間佳励,RSet
中的引用關(guān)系靠post-write barrier
和Concurrent refinement threads
來維護(hù)。
? ?當(dāng)發(fā)生YGC時(shí)桨嫁,掃描標(biāo)記對(duì)象時(shí)植兰,只需要選定目標(biāo)新生代Region
的RSet
作為根集,這些RSet
中記錄了Old → Young
的跨代引用璃吧,GC掃描時(shí)只需要掃描這些RSet
即可楣导,從而避免了掃描整個(gè)Old
區(qū)。而當(dāng)G1發(fā)生MixedGC
時(shí)畜挨,Old
類型的Region
也有RSet
記錄著Old → Old
的引用關(guān)系筒繁,而Old → Young
的跨代引用可以從Young
類型的Region
中得到噩凹,這樣在發(fā)生MixedGC
時(shí)也不會(huì)出現(xiàn)整堆掃描的情況,所以引入RSet
在很大程度上減少了大量的GC工作毡咏。
實(shí)際上G1中的
RSet
對(duì)內(nèi)存的開銷也并不小驮宴,當(dāng)JVM中分區(qū)較多且運(yùn)行時(shí)間較長(zhǎng)的情況下,這塊的內(nèi)存開銷可能會(huì)占用到20%
以上呕缭。
1.6堵泽、G1收集器總結(jié)
? ?G1收集器是開辟分區(qū)收集的里程碑,同時(shí)它也被稱為垃圾優(yōu)先收集器恢总,因?yàn)镚1會(huì)在后臺(tái)維護(hù)著一個(gè)優(yōu)先列表:CollectionSet(CSet)
迎罗,它記錄了GC要收集的Region
集合,集合里的Region
可以是任意年代的片仿。
? ?每次GC發(fā)生時(shí)會(huì)根據(jù)「用戶指定的期望停頓時(shí)間或默認(rèn)的期望停頓時(shí)間」纹安,優(yōu)先從列表中選擇「回收價(jià)值最大」Region
區(qū)回收,這也是G1名字的由來砂豌。比如:
一個(gè)
Region
花120ms
能回收10MB
垃圾厢岂,而另外一個(gè)Region
花80ms
能回收20MB
垃圾,在回收時(shí)間有限情況下阳距,G1當(dāng)然會(huì)優(yōu)先選擇后面這個(gè)“性價(jià)比”更高的Region
回收塔粒。這種使用Region
劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,能夠確保G1收集器在有限時(shí)間內(nèi)娄涩,可以盡可能達(dá)到更高的收集效率窗怒。
? ?毋庸置疑的一點(diǎn)映跟,可以由用戶指定「GC回收過程中的期望停頓時(shí)間」是G1的一大亮點(diǎn)蓄拣,通過設(shè)置不同的「期望停頓時(shí)間」可以使得G1在任意場(chǎng)景下,都能在「吞吐量」和「響應(yīng)時(shí)間/低延遲」之間取得很好的平衡努隙。但這里設(shè)置的「期望停頓時(shí)間」值必須要符合實(shí)際球恤,如果想著:如果設(shè)置成20ms
豈不是超低延遲了?這是并不現(xiàn)實(shí)的荸镊,只能稱為異想天開咽斧,因?yàn)镚1回收階段也需要發(fā)生STW的來根掃描、復(fù)制對(duì)象躬存、清理對(duì)象的张惹,所以這個(gè)「期望停頓時(shí)間」值也得有個(gè)度。與分代篇中分析PS收集器提到的一樣:
在追求響應(yīng)時(shí)間的時(shí)候必然會(huì)犧牲吞吐量岭洲,而追求吞吐量的同時(shí)必然會(huì)犧牲響應(yīng)時(shí)間宛逗。魚和熊掌不可兼得矣。
? ?G1的默認(rèn)的「停頓時(shí)間」目標(biāo)為200ms
盾剩,但一般而言雷激,實(shí)際過程中中占到[幾十 ~ 三百毫秒之間]都很正常替蔬,但如果我們把停頓時(shí)間調(diào)得非常低,譬如前面的20ms
屎暇, 很可能出現(xiàn)的結(jié)果就是:
由于停頓目標(biāo)時(shí)間太短承桥,導(dǎo)致每次選出來回收的目標(biāo)
Region
只占堆內(nèi)存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度根悼,最終造成垃圾慢慢堆積凶异,從而拖垮整個(gè)程序觸發(fā)單線程的FullGC
。
因?yàn)楹芸赡芤婚_始收集器還能從空閑的堆內(nèi)存中獲得一些喘息的時(shí)間挤巡,但程序運(yùn)行的時(shí)間越長(zhǎng)唠帝,對(duì)內(nèi)存造成的負(fù)擔(dān)就越重,最終占滿堆引發(fā)FullGC
玄柏,反而降低整體性能襟衰,所以實(shí)際應(yīng)用中,通常把「期望停頓時(shí)間」設(shè)置為100ms~300ms
之間會(huì)是比較合理的選擇粪摘。
1.7瀑晒、什么場(chǎng)景下適合采用G1收集器的建議
- ①堆空間內(nèi)
50%
以上的內(nèi)存會(huì)被存活占用的應(yīng)用 - ②分配速度和晉升速度特別快的應(yīng)用
- ③至少
8GB
以上堆內(nèi)存的應(yīng)用 - ④采用原本分代收集器GC時(shí)間會(huì)長(zhǎng)達(dá)
1s+
的應(yīng)用 - ⑤追求停頓時(shí)間在
500ms
以內(nèi)的應(yīng)用
二、一款源自于JDK11的性能魔獸 - ZGC
? ?在JDK11的時(shí)候徘意,Java再次推出一款全新的垃圾回收器ZGC
苔悦,它也是一款基于分區(qū)概念的內(nèi)存布局GC器,這款GC器是真正意義上的不分代收集器椎咧,因?yàn)樗鼰o論是從邏輯上玖详,還是物理上都不再保留分代的概念。
ZGC的內(nèi)存結(jié)構(gòu)實(shí)際上被稱為分頁勤讽,源于
Linux Kernel 2.6
中引入了標(biāo)準(zhǔn)的大頁huge page
蟋座,大頁存在兩種尺度,分別為2MB
以及1GB
脚牍。
Linux內(nèi)核引入大頁的目的主要在于為了迎合硬件發(fā)展向臀,因?yàn)樵谠朴?jì)算、彈性調(diào)度等技術(shù)的發(fā)展诸狭,服務(wù)器硬件的配置會(huì)越來越高券膀,如果再依照之前標(biāo)準(zhǔn)頁4KB
的大小劃分內(nèi)存,那最終反而會(huì)影響性能驯遇。
? ?ZGC
主打的是超低延遲與吞吐量芹彬,在實(shí)現(xiàn)時(shí),ZGC
也會(huì)在盡可能堆吞吐量影響不大的前提下叉庐,實(shí)現(xiàn)在任意堆內(nèi)存大小下都可以把垃圾回收的停頓時(shí)間限制在10ms
以內(nèi)的低延遲舒帮。ZGC最初是源于Azul System
公司的C4(Concurrent Continuously Compacting Collector)
收集器與PauselessGC
,Java引入ZGC的目的主要有如下四點(diǎn):
- ①奠定未來GC特性的基礎(chǔ)。
- ②為了支持超大級(jí)別堆空間(
TB
級(jí)別)会前,最高支持16TB
好乐。 - ③在最糟糕的情況下,對(duì)吞吐量的影響也不會(huì)降低超過15%瓦宜。
- ④GC觸發(fā)產(chǎn)生的停頓時(shí)間不會(huì)偏差
10ms
蔚万。
2.1、ZGC堆空間內(nèi)存劃分
? ?在ZGC中临庇,也會(huì)把堆空間劃分為一個(gè)個(gè)的Region
區(qū)域反璃,但ZGC中的Region
區(qū)不存在分代的概念,它僅僅只是簡(jiǎn)單的將所有Region
區(qū)分為了大假夺、中淮蜈、小三個(gè)等級(jí),如下:
- 小型區(qū)/頁(
Small
):固定大小為2MB
已卷,用于分配小于256KB
的對(duì)象梧田。 - 中型區(qū)/頁(
Medium
):固定大小為32MB
,用于分配>=256KB ~ <=4MB
的對(duì)象侧蘸。 - 大型區(qū)/頁(
Large
):沒有固定大小裁眯,容量可以動(dòng)態(tài)變化,但是大小必須為2MB
的整數(shù)倍讳癌,專門用于存放>4MB
的巨型對(duì)象穿稳。但值得一提的是:每個(gè)Large
區(qū)只能存放一個(gè)大對(duì)象,也就代表著你的這個(gè)大對(duì)象多大晌坤,那么這個(gè)Large
區(qū)就為多大逢艘,所以一般情況下,Large
區(qū)的容量要小于Medium
區(qū)骤菠,并且需要注意:Large
區(qū)的空間是不會(huì)被重新分配的(稍后分析)它改。
源碼中的注釋如下:
實(shí)際上JDK11中的ZGC并不是因?yàn)橐獟仐壏执砟疃辉O(shè)計(jì)分代的堆空間的,因?yàn)閷?shí)際上最開始分代理念被提出的本質(zhì)原因是源于「大部分對(duì)象朝生夕死」這個(gè)概念的娩怎,而實(shí)際上大部分Java程序在運(yùn)行時(shí)都符合這個(gè)現(xiàn)象搔课,所以邏輯分代+物理不分代是堆空間最好的結(jié)構(gòu)方案胰柑。但問題在于:ZGC為何不設(shè)計(jì)出分代的堆空間結(jié)構(gòu)呢截亦?其實(shí)本質(zhì)原因是分代實(shí)現(xiàn)起來非常麻煩且復(fù)雜,所以就先實(shí)現(xiàn)出一個(gè)比較簡(jiǎn)單可用的單代版本柬讨,后續(xù)可能會(huì)優(yōu)化改進(jìn)崩瓤。
2.1.1、TB級(jí)別內(nèi)存出處:NUMA架構(gòu)
? ?前面提到過:“ZGC的目的是為了能夠駕馭TB級(jí)別的超大堆空間”踩官,但問題在于很多小伙伴的生產(chǎn)環(huán)境中却桶,硬盤都不一定能夠達(dá)到TB級(jí)別,那服務(wù)器怎么能有TB
級(jí)的內(nèi)存用于分配Java堆呢?而想要搞明白這個(gè)點(diǎn)就不得不提及到NUMA
架構(gòu)颖系,當(dāng)然嗅剖,與之對(duì)應(yīng)的則為UMA
架構(gòu),如下:
UMA
架構(gòu):UMA
即Uniform Memory Access Architecture
(統(tǒng)一內(nèi)存訪問)嘁扼,UMA
也就是一般正常電腦的常用架構(gòu)信粮,一塊內(nèi)存多顆CPU,所有CPU在處理時(shí)都去訪問一塊內(nèi)存趁啸,所以必然就會(huì)出現(xiàn)競(jìng)爭(zhēng)(爭(zhēng)奪內(nèi)存主線訪問權(quán))强缘,而操作系統(tǒng)為了避免競(jìng)爭(zhēng)過程中出現(xiàn)安全性問題,注定著也會(huì)伴隨鎖概念存在不傅,有鎖在的場(chǎng)景定然就會(huì)影響效率旅掂。同時(shí)CPU訪問內(nèi)存都需要通過總線和北橋,因此當(dāng)CPU核數(shù)越來越多時(shí)访娶,漸漸的總線和北橋就成為瓶頸商虐,從而導(dǎo)致使用UMA/SMP
架構(gòu)機(jī)器CPU核數(shù)越多,競(jìng)爭(zhēng)會(huì)越大崖疤,性能會(huì)越低称龙。
NUMA
架構(gòu):NUMA
即Non Uniform Memory Access Architecture
(非統(tǒng)一內(nèi)存訪問),NUMA
架構(gòu)下戳晌,每顆CPU都會(huì)對(duì)應(yīng)有一塊內(nèi)存鲫尊,具體內(nèi)存取決于處理器的內(nèi)存位置,一般與CPU對(duì)應(yīng)的內(nèi)存都是在主板上離該CPU最近的沦偎,CPU會(huì)優(yōu)先訪問這塊內(nèi)存疫向,每顆CPU各自訪問距離自己最近的內(nèi)存,效率自然而然就提高了豪嚎。
但上述內(nèi)容并非重點(diǎn)搔驼,重點(diǎn)是NUMA
架構(gòu)允許多臺(tái)機(jī)器共同組成一個(gè)服務(wù)供給外部使用,NUMA
技術(shù)可以使眾多服務(wù)器像單一系統(tǒng)那樣運(yùn)轉(zhuǎn)侈询,該架構(gòu)在中大型系統(tǒng)上一直非常盛行舌涨,也是高性能的解決方案,尤其在系統(tǒng)延遲方面表現(xiàn)都很優(yōu)秀扔字,因此囊嘉,實(shí)際上堆空間也可以由多臺(tái)機(jī)器的內(nèi)存組成。
ZGC是能自動(dòng)感知NUMA架構(gòu)并可以充分利用NUMA架構(gòu)特性的一款垃圾收集器革为。
2.2扭粱、ZGC回收過程
ZGC收集器在發(fā)生GC時(shí),其實(shí)主要操作只有三個(gè):標(biāo)記震檩、轉(zhuǎn)移與重定位琢蛤。
- 標(biāo)記:從根節(jié)點(diǎn)出發(fā)標(biāo)記所有存活對(duì)象蜓堕。
- 轉(zhuǎn)移:將需要回收區(qū)域中的存活對(duì)象轉(zhuǎn)移到新的分區(qū)中。
- 重定位:將所有指向轉(zhuǎn)移前地址的指針更改為指向轉(zhuǎn)移后的地址博其。
實(shí)際上重定位動(dòng)作會(huì)在標(biāo)記階段中的執(zhí)行套才,在標(biāo)記的時(shí)候如果發(fā)現(xiàn)指針還是引用老的地址則會(huì)修正成新的地址,然后再進(jìn)行標(biāo)記慕淡。
但是值得注意的是:第一次GC發(fā)生時(shí)霜旧,并不會(huì)發(fā)生重定位動(dòng)作,因?yàn)橐呀?jīng)標(biāo)記完了儡率,這個(gè)時(shí)候只會(huì)記錄一下原本的對(duì)象被轉(zhuǎn)移到哪兒去了挂据。只有當(dāng)?shù)诙蜧C發(fā)生時(shí),開始標(biāo)記的時(shí)候發(fā)現(xiàn)某個(gè)對(duì)象被轉(zhuǎn)移了儿普,但引用還是老的崎逃,此時(shí)才會(huì)發(fā)生重定位操作,即修改成新的引用地址眉孩。
? ?ZGC中的一次垃圾回收過程會(huì)被分為十個(gè)步驟:初始標(biāo)記个绍、并發(fā)標(biāo)記、再次標(biāo)記浪汪、并發(fā)轉(zhuǎn)移準(zhǔn)備:[非強(qiáng)引用并發(fā)標(biāo)記巴柿、重置轉(zhuǎn)移集、回收無效頁面(區(qū))死遭、選擇目標(biāo)回收頁面广恢、初始化轉(zhuǎn)移集(表)]、初始轉(zhuǎn)移呀潭、并發(fā)轉(zhuǎn)移钉迷,其中只有初始標(biāo)記饰豺、再次標(biāo)記琼讽、初始轉(zhuǎn)移階段會(huì)存在短暫的STW,其他階段都是并發(fā)執(zhí)行的声旺。
ZGC回收過程的十個(gè)步驟中谐鼎,非強(qiáng)引用并發(fā)標(biāo)記舰蟆、重置轉(zhuǎn)移集、回收無效頁面(區(qū))狸棍、選擇目標(biāo)回收頁面身害、初始化轉(zhuǎn)移集(表)這些步驟都是并發(fā)的,都會(huì)發(fā)生在并發(fā)轉(zhuǎn)移準(zhǔn)備階段內(nèi)隔缀,如下:
- ①初始標(biāo)記
這個(gè)階段會(huì)觸發(fā)STW题造,僅標(biāo)記根可直達(dá)的對(duì)象,并將其壓入到標(biāo)記棧中猾瘸,在該階段中也會(huì)發(fā)生一些其他動(dòng)作,如重置 TLAB、判斷是否要清除軟引用等牵触。 - ②并發(fā)標(biāo)記
根據(jù)「初始標(biāo)記」的根對(duì)象開啟多條GC線程淮悼,并發(fā)遍歷對(duì)象圖,同時(shí)也會(huì)統(tǒng)計(jì)每個(gè)分區(qū)/頁面中的存活對(duì)象數(shù)量揽思。
標(biāo)記棧 - ③再次標(biāo)記
這個(gè)階段也會(huì)出現(xiàn)短暫的STW袜腥,因?yàn)椤覆l(fā)標(biāo)記」階段中應(yīng)用線程還是在運(yùn)行的,所以會(huì)修改對(duì)象的引用導(dǎo)致漏標(biāo)的情況出現(xiàn)钉汗,因此需要再次標(biāo)記階段來標(biāo)記漏標(biāo)的對(duì)象(如果此階段停頓時(shí)間過長(zhǎng)羹令,ZGC會(huì)再次進(jìn)入并發(fā)標(biāo)記階段重新標(biāo)記)。 - ④非強(qiáng)引用并發(fā)標(biāo)記和引用并發(fā)處理
遍歷前面過程中的非強(qiáng)引用類型根對(duì)象损痰,但并不是所有非強(qiáng)根對(duì)象都可并發(fā)標(biāo)記福侈,有部分不能并發(fā)標(biāo)記的非強(qiáng)根對(duì)象會(huì)再前面的「再次標(biāo)記」階段中處理。同時(shí)也會(huì)標(biāo)記堆中的非強(qiáng)引用類型對(duì)象卢未。 - ⑤重置轉(zhuǎn)移集/表
重置上一次GC發(fā)生時(shí)肪凛,轉(zhuǎn)移表中記錄的數(shù)據(jù),方便本次GC使用辽社。- 在ZGC中伟墙,因?yàn)樵诨厥諘r(shí)需要把一個(gè)分區(qū)中的存活對(duì)象轉(zhuǎn)移進(jìn)另外一個(gè)空閑分區(qū)中,而ZGC的轉(zhuǎn)移又是并發(fā)執(zhí)行的滴铅,因此戳葵,一條用戶線程訪問堆中的一個(gè)對(duì)象時(shí),該對(duì)象恰巧被轉(zhuǎn)移了汉匙,那么這條用戶線程根據(jù)原本的指針是無法定位對(duì)象的譬淳,所以在ZGC中引入了轉(zhuǎn)移表
forwardingTable
的概念。 - 轉(zhuǎn)移表可以理解為一個(gè)
Map<OldAddress,NewAddress>
結(jié)構(gòu)的集合盹兢,當(dāng)一條線程根據(jù)指針訪問一個(gè)被轉(zhuǎn)移的對(duì)象時(shí)邻梆,如果該對(duì)象已經(jīng)被轉(zhuǎn)移,則會(huì)根據(jù)轉(zhuǎn)移表的記錄去新地址中查找對(duì)象绎秒,并同時(shí)會(huì)更新指針的引用浦妄。
- 在ZGC中伟墙,因?yàn)樵诨厥諘r(shí)需要把一個(gè)分區(qū)中的存活對(duì)象轉(zhuǎn)移進(jìn)另外一個(gè)空閑分區(qū)中,而ZGC的轉(zhuǎn)移又是并發(fā)執(zhí)行的滴铅,因此戳葵,一條用戶線程訪問堆中的一個(gè)對(duì)象時(shí),該對(duì)象恰巧被轉(zhuǎn)移了汉匙,那么這條用戶線程根據(jù)原本的指針是無法定位對(duì)象的譬淳,所以在ZGC中引入了轉(zhuǎn)移表
- ⑥回收無效分區(qū)/頁面
回收物理內(nèi)存已經(jīng)被釋放的無效的虛擬內(nèi)存頁面。ZGC是一款支持返還堆內(nèi)存給物理機(jī)器的收集器见芹,在機(jī)器內(nèi)存緊張時(shí)會(huì)釋放一些未使用的堆空間剂娄,但釋放的頁面需要在新一輪標(biāo)記完成之后才能釋放,所以在這個(gè)階段其實(shí)回收的是上一次GC釋放的空間玄呛。 - ⑦選擇待回收的分區(qū)/頁面
ZGC與GC收集器一樣阅懦,也會(huì)存在「垃圾優(yōu)先」的特性,在標(biāo)記完成后徘铝,整個(gè)堆中會(huì)有很多分區(qū)可以回收耳胎,ZGC也會(huì)篩選出回收價(jià)值最大的頁面來作為本次GC回收的目標(biāo)惯吕。 - ⑧初始化待轉(zhuǎn)移集合的轉(zhuǎn)移表
初始化待回收分區(qū)/頁面的轉(zhuǎn)移表,方便記錄區(qū)中存活對(duì)象的轉(zhuǎn)移信息怕午。- 注:每個(gè)頁面/分區(qū)都存在一個(gè)轉(zhuǎn)移表
forwardingTable
废登。
- 注:每個(gè)頁面/分區(qū)都存在一個(gè)轉(zhuǎn)移表
- ⑨初始轉(zhuǎn)移
這個(gè)階段會(huì)發(fā)生STW,遍歷所有GCRoots
節(jié)點(diǎn)及其直連對(duì)象郁惜,如果遍歷到的對(duì)象在回收分區(qū)集合內(nèi)堡距,則在新的分區(qū)中為該對(duì)象分配對(duì)應(yīng)的空間。不過值得注意的是:該階段只會(huì)轉(zhuǎn)移根對(duì)象(也就是GCRoots
節(jié)點(diǎn)直連對(duì)象)兆蕉。 - ⑩并發(fā)轉(zhuǎn)移
這個(gè)階段與之前的「并發(fā)標(biāo)記」很相似羽戒,從上一步轉(zhuǎn)移的根對(duì)象出發(fā),遍歷目標(biāo)區(qū)域中的所有對(duì)象虎韵,做并發(fā)轉(zhuǎn)移處理易稠。
其實(shí)簡(jiǎn)單來說,ZGC的回收過程可以分為四大階段:并發(fā)標(biāo)記劝术、并發(fā)轉(zhuǎn)移準(zhǔn)備缩多、并發(fā)轉(zhuǎn)移、并發(fā)重映射/定位养晋。
? ?同時(shí)ZGC也是一款不分代的收集器衬吆,也就代表著ZGC中只存在一種GC類型,同時(shí)也不需要記憶集這種概念存在绳泉,因?yàn)槭菃未亩芽臻g逊抡,所以每次回收都是掃描所有頁面,不需要額外解決跨代引用問題零酪。
2.2.1冒嫡、為何Large區(qū)不能被重分配/轉(zhuǎn)移呢?
? ?因?yàn)?code>Large區(qū)中只會(huì)存儲(chǔ)一個(gè)對(duì)象四苇,在GC發(fā)生時(shí)標(biāo)記完成后孝凌,直接決定是否回收即可,Large
區(qū)中存儲(chǔ)的對(duì)象并非不能轉(zhuǎn)移到其他區(qū)月腋,而是沒有必要蟀架,本身當(dāng)前Large
區(qū)中就只有一個(gè)大對(duì)象,轉(zhuǎn)移還得專門準(zhǔn)備另外一個(gè)Large
區(qū)接收榆骚,但本質(zhì)上轉(zhuǎn)不轉(zhuǎn)移都不會(huì)影響片拍,反而會(huì)增加額外的空間開銷。
2.2.2妓肢、ZGC的核心 - 染色指針(colored pointers)技術(shù)
? ?ColoredPointers
捌省,ZGC的核心技術(shù)之一,在此之前所有的GC信息保存在對(duì)象頭中碉钠,但ZGC中的GC信息保存在指針內(nèi)纲缓。同時(shí)卷拘,在ZGC中不存在指針壓縮,因?yàn)閆GC中對(duì)于指針進(jìn)行了改造色徘,通過程序中的引用指針來實(shí)現(xiàn)了染色指針技術(shù)恭金,由于染色指針對(duì)于指針的64個(gè)比特位全部都使用了操禀,所以指針無法再進(jìn)行壓縮褂策。
? ?染色指針也有很多其他稱呼,諸如:顏色指針颓屑、著色指針等斤寂,其實(shí)都是一個(gè)意思,無非就是將64位指針中的幾位拿出來用于標(biāo)記對(duì)象此時(shí)的情況揪惦,ZGC中的染色指針用到了四種“顏色(狀態(tài))”遍搞,分別為:Marked0、Marked1器腋、Remapped溪猿、Finalizable
,如下:
對(duì)于上圖中指針的釋義纫塌,源碼注釋如下:
? ?從注釋中可看出:
0~41Bit
這42位是正常的地址記錄诊县,所以ZGC實(shí)際上最大能夠支持4TB
(理論上最大可以支持16TB
)的堆內(nèi)存,因?yàn)?2位的地址最大尋址為4TB(2^42=4TB)
措左。除開這42個(gè)地址位外依痊,其他的位數(shù)釋義如下:
-
42~45Bit
/4位:標(biāo)志位-
Finalizable=1000
:此位與并發(fā)引用處理有關(guān),表示這個(gè)對(duì)象只能通過finalizer
才能訪問怎披。 -
Remapped=0100
:設(shè)置此位的值后胸嘁,表示這個(gè)對(duì)象未指向RelocationSet
中(relocation set
表示需要GC的Region
分區(qū)/頁面集合)。 -
Marked1=0010
:標(biāo)記對(duì)象凉逛,用于輔助GC性宏。 -
Marked0=0001
:標(biāo)記對(duì)象,用于輔助GC状飞。
-
-
46~63Bit
/18位:預(yù)留位毫胜,預(yù)留給以后使用。
不過我們?cè)诜治鼍唧w實(shí)現(xiàn)前昔瞧,先來探討幾個(gè)問題指蚁。
①ZGC為何僅支持
4TB
(JDK13拓展到了16TB
),不是還有很多位沒用嗎自晰?
前面分析指針后得知凝化,ZGC在指針中預(yù)留了18位沒使用,那為什么不全部用上酬荞,讓ZGC支持超級(jí)巨大的堆空間搓劫。其實(shí)這跟硬件和OS有關(guān)瞧哟,因?yàn)?code>X86_64架構(gòu)的硬件只有48條地址總線,硬件的主板地址總線最寬只有48bit
枪向,其中4個(gè)是顏色位勤揩,就只剩下了44位了,所以受限于目前的硬件秘蛔,ZGC最大就只能支持2^44=16TB
內(nèi)存陨亡。
②為什么會(huì)有兩個(gè)
Marked
標(biāo)識(shí)?
這是為了防止不同GC周期之間的標(biāo)記混淆深员,所以搞了兩個(gè)Marked
標(biāo)識(shí)负蠕,每當(dāng)新的一次GC開始時(shí),都會(huì)交換使用的標(biāo)記位倦畅。例如:第一次GC使用M0
遮糖,第二次GC就會(huì)使用M1
,第三次又會(huì)使用M0
.....叠赐,因?yàn)閆GC標(biāo)記完所有分區(qū)的存活對(duì)象后欲账,會(huì)選擇分區(qū)進(jìn)行回收,因此有一部分區(qū)域內(nèi)的存活對(duì)象不會(huì)被轉(zhuǎn)移芭概,那么這些對(duì)象的標(biāo)識(shí)就不會(huì)復(fù)位赛不,會(huì)停留在之前的Marked
標(biāo)識(shí)(比如M0
),如果下次GC還是使用相同M0
來標(biāo)記對(duì)象谈山,那混淆了這兩種對(duì)象俄删。為了確保標(biāo)記不會(huì)混淆,所以搞了兩個(gè)Marked
標(biāo)識(shí)交替使用奏路。
③為什么ZGC運(yùn)行過程中指針的標(biāo)記位變動(dòng)不會(huì)影響對(duì)象的內(nèi)存尋址畴椰?
這是因?yàn)閆GC的染色指針用到了一種叫做多重映射的技術(shù),也就是指多個(gè)虛擬地址指向同一個(gè)物理地址鸽粉,不管指針地址是0001....
還是0010.....
斜脂,或者0100....
等等,最終對(duì)應(yīng)的都是同一個(gè)物理地址触机。
OK帚戳,簡(jiǎn)單分析明白上述幾個(gè)問題之后,再來看看ZGC基于染色指針的并發(fā)處理過程:
- 在第一次GC發(fā)生前儡首,堆中所有對(duì)象的標(biāo)識(shí)為:
Remapped
片任。 - 第一次GC被觸發(fā)后,GC線程開始標(biāo)記蔬胯,開始掃描对供,如果對(duì)象是
Remapped
標(biāo)志,并且該對(duì)象根節(jié)點(diǎn)可達(dá)的,則將其改為M0
標(biāo)識(shí)产场,表示存活對(duì)象鹅髓。 - 如果標(biāo)記過程中,掃描到的對(duì)象標(biāo)識(shí)已經(jīng)為
M0
京景,代表該對(duì)象已經(jīng)被標(biāo)記過窿冯,或者是GC開始后新分配的對(duì)象,這種情況下無需處理确徙。 - 在GC開始后醒串,用戶線程新創(chuàng)建的對(duì)象,會(huì)直接標(biāo)識(shí)為
M0
米愿。 - 在標(biāo)記階段厦凤,GC線程僅標(biāo)記用戶線程可直接訪問的對(duì)象還是不夠的鼻吮,實(shí)際上還需要把對(duì)象的成員變量所引用的對(duì)象都進(jìn)行遞歸標(biāo)記育苟。
總歸而言,在「標(biāo)記階段」結(jié)束后椎木,對(duì)象要么是M0
存活狀態(tài)违柏,要么是Remapped
待回收狀態(tài)。最終香椎,所有被標(biāo)記為M0
狀態(tài)的活躍對(duì)象都會(huì)被放入「活躍信息表」中漱竖。等到了「轉(zhuǎn)移階段」再對(duì)這些對(duì)象進(jìn)行處理,流程如下:
- ZGC選擇目標(biāo)回收區(qū)域畜伐,開始并發(fā)轉(zhuǎn)移馍惹。
- GC線程遍歷訪問目標(biāo)區(qū)域中的對(duì)象,如果對(duì)象標(biāo)識(shí)為
M0
并且存在于活躍表中玛界,則把該對(duì)象轉(zhuǎn)移到新的分區(qū)/頁面空間中万矾,同時(shí)將其標(biāo)識(shí)修正為Remapped
標(biāo)志。 - GC線程如果掃描到的對(duì)象存在于活躍表中慎框,但標(biāo)識(shí)為
Remapped
良狈,說明該對(duì)象已經(jīng)轉(zhuǎn)移過了,無需處理笨枯。 - 用戶線程在「轉(zhuǎn)移階段」新創(chuàng)建的對(duì)象薪丁,會(huì)被標(biāo)識(shí)為
Remapped
。 - 如果GC線程遍歷到的對(duì)象不是
M0
狀態(tài)或不在活躍表中馅精,也無需處理严嗜。
? ?最終,當(dāng)目標(biāo)區(qū)域中的所有存活對(duì)象被轉(zhuǎn)移到新的分區(qū)后洲敢,ZGC統(tǒng)一回收原本的選擇的回收區(qū)域漫玄。至此,一輪GC結(jié)束沦疾,整個(gè)堆空間會(huì)正常執(zhí)行下去称近,直至觸發(fā)下一輪GC第队。而當(dāng)下一輪GC發(fā)生時(shí),會(huì)采用M1
作為GC輔助標(biāo)識(shí)刨秆,而并非M0
凳谦,具體原因在前面分析過了則不再闡述。
PS:在有些地方也把指針中的幾個(gè)標(biāo)識(shí)稱為:地址視圖衡未。簡(jiǎn)單來說尸执,地址視圖指的就是此時(shí)地址指針的標(biāo)記位,比如標(biāo)記位現(xiàn)在是
M0
缓醋,那么此時(shí)的視圖就是M0
視圖如失。
染色指針帶來的好處
- ①一旦某個(gè)分區(qū)中的存活對(duì)象被移走,該分區(qū)就可以立即回收并重用送粱,不必等到整個(gè)堆中所有指向該
Region
區(qū)的引用都被修正后才能清理褪贵。 - ②顏色指針可以大幅減少在GC過程中內(nèi)存屏障的使用數(shù)量,ZGC只使用了讀屏障抗俄。
- ③顏色指針具備強(qiáng)大的擴(kuò)展性脆丁,它可以作為一種可擴(kuò)展的存儲(chǔ)結(jié)構(gòu)用來記錄更多與對(duì)象標(biāo)記、重定位過程相關(guān)的數(shù)據(jù)动雹,以便日后進(jìn)一步提高性能槽卫。
2.2.3、何謂轉(zhuǎn)移表/集(ForwardingTable)胰蝠?
? ?轉(zhuǎn)移表ForwardingTable
是ZGC確保轉(zhuǎn)移對(duì)象后歼培,其他引用指針能夠指向最新地址的一種技術(shù),每個(gè)頁面/分區(qū)中都會(huì)存在茸塞,其實(shí)就是該區(qū)中所有存活對(duì)象的轉(zhuǎn)移記錄躲庄,一條線程通過引用來讀取對(duì)象時(shí),發(fā)現(xiàn)對(duì)象被轉(zhuǎn)移后就會(huì)去轉(zhuǎn)移表中查詢最新的地址翔横。同時(shí)轉(zhuǎn)移表中的數(shù)據(jù)會(huì)在發(fā)生第二次GC時(shí)清空重置读跷,也包括會(huì)在第二次GC時(shí)觸發(fā)重映射/重定位操作。
2.3禾唁、ZGC - 讀屏障解決對(duì)象漏標(biāo)
? ?之前曾提及過:ZGC是通過讀屏障的手段解決了對(duì)象漏標(biāo)問題效览,讀屏障也就相當(dāng)于讀取引用時(shí)的AOP,偽代碼如下:
oop oop_field_load(oop* field) {
pre_load_barrier(field); // 讀屏障-讀前操作
return *field;
}
void pre_load_barrier(oop* field, oop old_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value = *field;
remark_set.add(old_value); // 記錄讀取到的對(duì)象
}
}
? ?讀屏障是在讀取成員變量時(shí)荡短,統(tǒng)統(tǒng)記錄下來丐枉,這種做法是保守的,但也是安全的掘托。因?yàn)榍笆龇治鰧?duì)象漏標(biāo)問題時(shí)瘦锹,曾談到過:引發(fā)漏標(biāo)問題必須要滿足兩個(gè)條件,其中條件二為:「已經(jīng)標(biāo)為黑色的對(duì)象重新與白色對(duì)象建立了引用關(guān)系」,而黑色對(duì)象想要與白色對(duì)象重新建立引用的前提是:得先讀取到白色對(duì)象弯院,此時(shí)讀屏障的作用就出來了辱士,可以直接記錄誰讀取了當(dāng)前白色對(duì)象,然后在「再次標(biāo)記」重新標(biāo)記一下這些黑色對(duì)象即可听绳。
通過讀屏障還有另外的作用颂碘,GC發(fā)生后,堆中一部分存活對(duì)象被轉(zhuǎn)移椅挣,當(dāng)應(yīng)用線程讀取對(duì)象時(shí)头岔,可以利用讀屏障通過指針上的標(biāo)志來判斷對(duì)象是否被轉(zhuǎn)移,如果讀取的對(duì)象已經(jīng)被轉(zhuǎn)移鼠证,那么則修正當(dāng)前對(duì)象引用為最新地址(去轉(zhuǎn)移表中查)峡竣。這樣做的好處在于:下次其他線程再讀取該轉(zhuǎn)移對(duì)象時(shí),可以正常訪問讀取到最新值量九。
當(dāng)然适掰,這種情況在有些地方也被稱為:ZGC的指針擁有“自愈”的能力。
2.4娩鹉、ZGC的垃圾回收什么情況下會(huì)被觸發(fā)攻谁?
ZGC中目前會(huì)有四種機(jī)制導(dǎo)致GC被觸發(fā):
- ①定時(shí)觸發(fā),默認(rèn)為不使用弯予,可通過
ZCollectionInterval
參數(shù)配置。 - ②預(yù)熱觸發(fā)个曙,最多三次锈嫩,在堆內(nèi)存達(dá)到
10%、20%垦搬、30%
時(shí)觸發(fā)呼寸,主要時(shí)統(tǒng)計(jì)GC時(shí)間,為其他GC機(jī)制使用猴贰。 - ③分配速率对雪,基于正態(tài)分布統(tǒng)計(jì),計(jì)算內(nèi)存
99.9%
可能的最大分配速率米绕,以及此速率下內(nèi)存將要耗盡的時(shí)間點(diǎn)瑟捣,在耗盡之前觸發(fā)GC「耗盡時(shí)間 - 一次GC最大持續(xù)時(shí)間 - 一次GC檢測(cè)周期時(shí)間」。 - ④主動(dòng)觸發(fā)栅干,默認(rèn)開啟迈套,可通過
ZProactive
參數(shù)配置,距上次GC堆內(nèi)存增長(zhǎng)10%碱鳞,或超過5分鐘時(shí)桑李,對(duì)比「距上次GC的間隔時(shí)間」和「49*一次GC的最大持續(xù)時(shí)間」,超過則觸發(fā)。
2.5贵白、ZGC總結(jié)
? ?ZGC因?yàn)槭腔?4位指針實(shí)現(xiàn)的染色指針技術(shù)率拒,所以也就注定了ZGC并不能支持32位的機(jī)器。同時(shí)禁荒,ZGC通過多階段的并發(fā)執(zhí)行+幾個(gè)短暫的STW階段來達(dá)到低延遲的目的俏橘。
? ?ZGC最大的問題是浮動(dòng)垃圾,假設(shè)ZGC的一次完整GC需要八分鐘圈浇,在這期間由于新對(duì)象的分配速率很高寥掐,所以堆中會(huì)產(chǎn)生大量的新對(duì)象,這些新對(duì)象是不會(huì)被計(jì)入本次GC的磷蜀,會(huì)被直接判定為存活對(duì)象召耘,而本輪GC回收期間可能新分配的對(duì)象會(huì)有大部分對(duì)象都成為了“垃圾”,但這些“浮動(dòng)垃圾”只能等待下次GC的時(shí)候進(jìn)行回收褐隆。
? ?ZGC不會(huì)因?yàn)槎芽臻g的擴(kuò)大而增大停頓時(shí)間的原因在于:ZGC只會(huì)在處理根節(jié)點(diǎn)等階段才會(huì)出現(xiàn)STW污它,而堆空間再怎么擴(kuò)大,內(nèi)存中的根節(jié)點(diǎn)數(shù)量不會(huì)出現(xiàn)質(zhì)的增長(zhǎng)庶弃,所以ZGC的停頓時(shí)間幾乎不受限于內(nèi)存大小。
? ?同時(shí)歇攻,ZGC與之前的收集器還有一點(diǎn)很大的不同在于:ZGC標(biāo)記的是指針而并非對(duì)象固惯,但最終達(dá)到的效果是等價(jià)的,因?yàn)樗袑?duì)象以及所有指針都會(huì)被遍歷缴守。
? ?在標(biāo)記和轉(zhuǎn)移的階段葬毫,每次從堆中讀取一個(gè)指針時(shí),這個(gè)指針都會(huì)經(jīng)過LVB(Loaded Value Barrier)
讀屏障屡穗。這個(gè)讀屏障會(huì)在不同的階段做不同的事情贴捡,在標(biāo)記階段,它會(huì)把指針“修正”成最新的對(duì)象地址值村砂;而在轉(zhuǎn)移階段烂斋,這個(gè)屏障會(huì)把讀出的指針更新到對(duì)象的新地址上,并且把堆里的這個(gè)指針“修正”到原本的字段里础废。這樣做的好處在于:就算GC時(shí)把對(duì)象移動(dòng)了汛骂,讀屏障也會(huì)發(fā)現(xiàn)并修正指針,于是對(duì)于用戶線程層面而言色迂,就永遠(yuǎn)都會(huì)持有更新后的有效指針香缺,而不需要通過stop-the-world
這種最粗粒度的同步方式來讓GC與應(yīng)用之間同步。
寫在最后的話:ZGC的不分代其實(shí)是它的缺點(diǎn)歇僧,因?yàn)閷?duì)象都是滿足朝生夕死的特性图张,ZGC不分代只是因?yàn)榉执容^難實(shí)現(xiàn)锋拖。
三、一個(gè)來自JDK12的性能神獸 - ShenandoahGC
? ?在JDK11中推出ZGC后祸轮,JDK12馬不停蹄的推出了ShenandoahGC
收集器兽埃,它與G1、ZGC
收集器一樣适袜,都是基于分區(qū)結(jié)構(gòu)實(shí)現(xiàn)的一款收集器柄错。和ZGC對(duì)比,相同的是:它們的停頓時(shí)間都不會(huì)受到堆空間大小的影響苦酱,但ShenandoahGC
與ZGC不同的是:
ZGC是基于
colored pointers
染色指針實(shí)現(xiàn)的售貌,而ShenandoahGC
是基于brooks pointers
轉(zhuǎn)發(fā)指針實(shí)現(xiàn)。
? ?ShenandoahGC
的內(nèi)存布局與G1很相似疫萤,也會(huì)將堆內(nèi)存劃分為一個(gè)個(gè) 大小相同的Region
區(qū)域颂跨,也同樣有存放大對(duì)象的Humongous
區(qū),你可以把ShenandoahGC
看做G1收集器的修改版扯饶,它比G1收集器實(shí)現(xiàn)上來說更為激進(jìn)恒削,一味追求極致低延遲。但ShenandoahGC
和ZGC一樣尾序,也沒有實(shí)現(xiàn)分代的架構(gòu)钓丰,所以在觸發(fā)GC時(shí)也不會(huì)有新生代、年老代之說每币,只會(huì)存在一種覆蓋全局的GC類型携丁。
3.1、ShenandoahGC收集過程
? ?ShenandoahGC
的一次垃圾回收會(huì)由兩個(gè)STW階段以及兩個(gè)并發(fā)執(zhí)行階段組成脯爪,在的GC被觸發(fā)后则北,會(huì)開始對(duì)整個(gè)堆空間進(jìn)行垃圾收集,過程如下:
- 初始標(biāo)記階段:標(biāo)記
GCRoots
直接可達(dá)的對(duì)象痕慢,會(huì)發(fā)生STW,但非常短暫涌矢。 - 并發(fā)標(biāo)記階段:和用戶線程一同工作掖举,從根對(duì)象出發(fā),標(biāo)記堆中所有對(duì)象娜庇。
- 最終標(biāo)記階段:同比
G1塔次、ZGC
中的重新標(biāo)記階段,會(huì)觸發(fā)STW名秀,會(huì)在該階段中修正并發(fā)標(biāo)記過程中由于用戶線程修改引用關(guān)系的導(dǎo)致的漏標(biāo)錯(cuò)標(biāo)對(duì)象励负,使用STAB機(jī)制實(shí)現(xiàn)。同時(shí)在該階段中也會(huì)選擇出回收價(jià)值最大的區(qū)域作為目標(biāo)區(qū)域等待回收匕得。 - 并發(fā)回收階段:與用戶線程并發(fā)執(zhí)行继榆,會(huì)待回收區(qū)域中的存活對(duì)象復(fù)制到其他未使用的
Region
區(qū)中去巾表,然后會(huì)將原本的Region
區(qū)全部清理并回收。
? ?上述過程中略吨,前面的階段與G1
差異不大集币,重點(diǎn)在于最后的回收階段,它是與用戶線程并發(fā)執(zhí)行的翠忠,所以也會(huì)造成新的問題出現(xiàn):
問題①:回收過程中鞠苟,如果一個(gè)對(duì)象被復(fù)制到新的區(qū)域,用戶線程通過原本指針訪問時(shí)如何定位對(duì)象呢秽之?
問題②:在并發(fā)回收過程中当娱,如果復(fù)制的時(shí)候出現(xiàn)了安全性問題怎么辦?
? ?這兩個(gè)問題在ShenandoahGC
中考榨,前者通過了BrooksPointers
轉(zhuǎn)發(fā)指針解決跨细,而后者則建立在轉(zhuǎn)發(fā)指針的基礎(chǔ)上,采用了讀+寫屏障解決董虱,接下來看看ShenandoahGC
的核心實(shí)現(xiàn):BrooksPointers
轉(zhuǎn)發(fā)指針扼鞋。
3.2、ShenandoahGC核心-BrooksPointers轉(zhuǎn)發(fā)指針
? ?當(dāng)對(duì)象被復(fù)制到新的區(qū)域時(shí)愤诱,用戶線程如何根據(jù)指針定位到最新的對(duì)象地址呢云头?在前面的ZGC中可以通過染色指針+讀屏障的方案獲取到最新的地址,但在ShenandoahGC
中卻提出了另一種方案:BrooksPointers
轉(zhuǎn)發(fā)指針淫半。
所謂的轉(zhuǎn)發(fā)指針就是在每個(gè)對(duì)象的對(duì)象頭前面添加了一個(gè)新的字段溃槐,也就是對(duì)象頭前面多了根指針。對(duì)于未移動(dòng)的對(duì)象而言科吭,指針指向的地址是自己昏滴,但對(duì)于移動(dòng)的對(duì)象而言,該指針指向的為對(duì)象新地址中的
BrooksPointers
轉(zhuǎn)發(fā)指針对人,示意圖如下:
因?yàn)?code>ShenandoahGC采用了轉(zhuǎn)發(fā)指針技術(shù)谣殊,所以當(dāng)用戶線程訪問一個(gè)對(duì)象時(shí),需要首先先找到
BrooksPointers
牺弄,再通過該指針中的地址找到對(duì)象成福。
同時(shí)缀辩,如果對(duì)象被移動(dòng)后饭豹,對(duì)象訪問的流程就變成了這樣:先找到舊的
BrooksPointers
原叮,再根據(jù)舊的轉(zhuǎn)發(fā)指針找到新的BrooksPointers
,然后再根據(jù)新的轉(zhuǎn)發(fā)指針找到對(duì)象咱台。
相當(dāng)于添加了一個(gè)句柄池的機(jī)制络拌,類似于句柄訪問的方式。
ShenandoahGC
通過這種技術(shù)解決了被移動(dòng)對(duì)象的訪問問題回溺,但帶來弊端也很明顯春贸,每次訪問都需要多上至少一次額外轉(zhuǎn)發(fā)混萝。
再來看看由于并發(fā)回收導(dǎo)致的線程安全問題,情況如下:
- ①GC線程正在復(fù)制舊對(duì)象去到新的區(qū)域祥诽。
- ②用戶線程此時(shí)更新了原本對(duì)象的數(shù)據(jù)譬圣。
- ③GC線程將原本舊對(duì)象的轉(zhuǎn)發(fā)指針指向新對(duì)象的轉(zhuǎn)發(fā)指針。
? ?分析如上情況可以得知雄坪,因?yàn)镚C線程已經(jīng)復(fù)制對(duì)象了厘熟,只是還沒來得及更新舊對(duì)象的轉(zhuǎn)發(fā)指針,所以導(dǎo)致了用戶操作落到了舊對(duì)象上面维哈,從而出現(xiàn)了安全問題绳姨。而ShenandoahGC
中則采用讀、寫屏障確保了步驟①阔挠、③是原子性的飘庄,從而解決了該問題。
3.3购撼、ShenandoahGC的連接矩陣
? ?在G1中解決跨區(qū)引用是通過RSet
這種記憶集的方式實(shí)現(xiàn)跪削,而在ShenandoahGC
為了記錄跨區(qū)的對(duì)象引用,也提出了一種新的概念:連接矩陣迂求,連接矩陣其實(shí)本質(zhì)上類似于一個(gè)二維數(shù)組碾盐,如果第N
個(gè)Region
區(qū)中有對(duì)象指向RegionM
區(qū)的對(duì)象,那么就在矩陣的N行M列中打上一個(gè)標(biāo)記:
? ?如上圖揩局,
R-3
區(qū)的對(duì)象A
引用了R-5
區(qū)的對(duì)象B
毫玖,而對(duì)象B
又引用了R-7
區(qū)中的對(duì)象C
,那么最終在矩陣上凌盯,這些跨區(qū)引用對(duì)象所在位置就會(huì)被打上對(duì)應(yīng)的標(biāo)記付枫,在回收時(shí)通過這張矩陣圖就可以得出哪些Region
之間產(chǎn)生了跨區(qū)引用。
四驰怎、高性能垃圾收集器總結(jié)
? ?在三款高性能的GC器中阐滩,就目前而言,唯一保留了分代思想的是G1县忌,而ZGC叶眉、ShenandoahGC
并非是因?yàn)椴环执阅芎靡恍┒粚?shí)現(xiàn)的,而是因?yàn)閷?shí)現(xiàn)難度大所以才沒有實(shí)現(xiàn)芹枷,在之前就曾提及過:邏輯分代+物理分區(qū)的結(jié)構(gòu)才是最佳的,所以不分代的結(jié)構(gòu)對(duì)于ZGC莲趣、ShenandoahGC
來說鸳慈,其實(shí)是一個(gè)缺點(diǎn),因?yàn)椴环执亩芽臻g喧伞,每次觸發(fā)GC時(shí)都會(huì)掃描整堆走芋。
? ?G1收集器在后續(xù)的JDK版本中一直在做優(yōu)化绩郎,因?yàn)镚1是打算作為全能的默認(rèn)GC器來研發(fā)的,但G1收集器最大的遺憾和短板在于:回收階段需要發(fā)生STW翁逞,所以導(dǎo)致了使用G1收集器的程序會(huì)出現(xiàn)不短的停頓肋杖。
? ?而ZGC、ShenandoahGC
兩款收集器挖函,前者采用了染色指針+讀屏障技術(shù)做到了并發(fā)回收状植,后者通過轉(zhuǎn)發(fā)指針+讀寫屏障也實(shí)現(xiàn)了并發(fā)回收。因此怨喘,使用這兩款收集器的應(yīng)用程序津畸,在運(yùn)行期間觸發(fā)GC時(shí),造成的停頓會(huì)非常短暫必怜,所以如果你的項(xiàng)目對(duì)延遲要求非常低肉拓,那么它兩個(gè)是很不錯(cuò)的選擇。
? ?不過ZGC由于承諾了最大不超過10ms
的低延遲梳庆,所以最惡劣的情況可能會(huì)導(dǎo)致降低15%
左右的吞吐量暖途,因此,如果你想使用它膏执,那么要做好擴(kuò)大堆空間的準(zhǔn)備驻售,因?yàn)橹荒芡ㄟ^加大堆空間來做到提升吞吐量。而ShenandoahGC
因?yàn)轭~外增加了轉(zhuǎn)發(fā)指針胧后,所以也存在兩個(gè)問題:
? ?? ?①訪問對(duì)象時(shí)芋浮,速度會(huì)更慢,因?yàn)樾枰辽俳?jīng)過一次地址轉(zhuǎn)發(fā)壳快。
? ?? ?②需要更多的空間存儲(chǔ)多出來的這根指針纸巷。
同時(shí),ShenandoahGC
是沒有給出類似于ZGC的“最大10ms
的低延遲”承諾眶痰,所以就現(xiàn)階段而言瘤旨,ShenandoahGC
性能會(huì)比ZGC差一些,但唯一的優(yōu)勢(shì)在于:它可以比ZGC支持更大的堆空間(雖然沒啥用)竖伯。
4.1存哲、G1、ZGC與ShenandoahGC區(qū)別
對(duì)比項(xiàng) | G1 | ZGC | ShenandoahGC |
---|---|---|---|
是否支持并發(fā)回收 | 不支持 | 支持 | 支持 |
最大堆空間大小 | 達(dá)到上百GB停頓時(shí)間會(huì)很長(zhǎng) | 16TB | 256TB |
平均停頓 | 500ms以內(nèi) | 10ms以內(nèi) | 1~20ms左右 |
是否支持指針壓縮 | 支持 | 不支持 | 支持 |
其實(shí)從上面的數(shù)據(jù)來看七婴,好像G1收集器壓根比不上其他兩款祟偷,但實(shí)際上并非如此,因?yàn)槊靠钍占鞫紩?huì)有自己的適用場(chǎng)景打厘,就好比在幾百MB
的堆空間中修肠,裝載ZGC就一定比G1好嗎?其實(shí)是不見得的户盯。因?yàn)镚1中存在分代的邏輯嵌施,而ZGC是單代的饲化,所以如果在分配速率較快的情況下,ZGC可能會(huì)跟不上(因?yàn)閆GC的整個(gè)GC過程很久)吗伤,而G1則可以完全勝任吃靠。
同時(shí),由于ZGC的染色指針使用了64位指針實(shí)現(xiàn)足淆,所以也就代表著:在ZGC中指針壓縮失效了巢块,所以在
32GB
以下的堆空間中,相同的對(duì)象數(shù)據(jù)缸浦,ZGC會(huì)比其他的收集器占用的空間更多夕冲。