(八)JVM成神路之GC不分代篇:G1牛哺、ZGC陋气、ShenandoahGC高性能收集器深入剖析

引言

? ?在《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í)奴曙,引入了可伸縮的低延遲垃圾回收器ZGCExperimental)别凹。
  • 2019年3月:JDK12發(fā)布,增強(qiáng)G1收集器洽糟,實(shí)現(xiàn)自動(dòng)返還未用堆內(nèi)存給操作系統(tǒng)。同時(shí)种蘸,引入了低停頓時(shí)間的收集器ShenandoahGCExperimental)蹋绽。
  • 2019年9月:JDK13發(fā)布,增強(qiáng)ZGC收集器嘱丢,實(shí)現(xiàn)自動(dòng)返還未用堆內(nèi)存給操作系統(tǒng)。
  • 2020年3月:JDK14發(fā)布祠饺,剔除了CMS收集器越驻,同時(shí)擴(kuò)展ZGCmacOSWindows上的應(yīng)用,增強(qiáng)G1支持NUMA架構(gòu)吠裆。

? ?從如上JDK的發(fā)布日志中可以得出三款新的GC收集器伐谈,分別為:Epsilon、ZGC试疙、ShenandoahGC诵棵,從此之后,JVM中的“GC家族”正式湊齊了十款收集器祝旷,如下圖:

Java十款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è)樣子:

JDK8的堆構(gòu)成

? ?到了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):

JDK9的內(nèi)存布局

? ?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收集器執(zhí)行過程

? ?在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诊沪,如下圖:

Region區(qū)結(jié)構(gòu)

? ?五個(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)記過程:
    STAB過程
  • ①階段是初始標(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ì)象建立起了引用連接。如下圖:

三色標(biāo)記-漏標(biāo)問題-情況①

白色對(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)系异吻。如下圖:

三色標(biāo)記-漏標(biāo)問題-情況②

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 barrierConcurrent refinement threads來維護(hù)。

? ?當(dāng)發(fā)生YGC時(shí)桨嫁,掃描標(biāo)記對(duì)象時(shí)植兰,只需要選定目標(biāo)新生代RegionRSet作為根集,這些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è)Region120ms能回收10MB垃圾厢岂,而另外一個(gè)Region80ms能回收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í),如下:

ZGC的堆結(jié)構(gòu)

  • 小型區(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ì)被重新分配的(稍后分析)它改。

源碼中的注釋如下:

ZGC分區(qū)源碼注釋

實(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):UMAUniform 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):NUMANon 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)隔缀,如下:

ZGC收集器執(zhí)行過程

  • ①初始標(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ì)更新指針的引用浦妄。
  • ⑥回收無效分區(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废登。
  • ⑨初始轉(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,如下:

ZGC-染色指針

對(duì)于上圖中指針的釋義纫塌,源碼注釋如下:
ZGC-染色指針-源碼注釋

? ?從注釋中可看出: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)行垃圾收集,過程如下:

ShenandoahGC收集器執(zhí)行過程

  • 初始標(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ā)指針对人,示意圖如下:

ShenandoahGC的轉(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)記:

ShenandoahGC-連接矩陣

? ?如上圖揩局,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ì)比其他的收集器占用的空間更多夕冲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市裂逐,隨后出現(xiàn)的幾起案子歹鱼,更是在濱河造成了極大的恐慌,老刑警劉巖卜高,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弥姻,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡掺涛,警方通過查閱死者的電腦和手機(jī)庭敦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薪缆,“玉大人秧廉,你說我怎么就攤上這事〖鹈保” “怎么了疼电?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)减拭。 經(jīng)常有香客問我蔽豺,道長(zhǎng),這世上最難降的妖魔是什么拧粪? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任修陡,我火速辦了婚禮,結(jié)果婚禮上可霎,老公的妹妹穿的比我還像新娘魄鸦。我一直安慰自己,他們只是感情好癣朗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布号杏。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盾致。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天荣暮,我揣著相機(jī)與錄音庭惜,去河邊找鬼。 笑死穗酥,一個(gè)胖子當(dāng)著我的面吹牛护赊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砾跃,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼骏啰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了抽高?” 一聲冷哼從身側(cè)響起判耕,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翘骂,沒想到半個(gè)月后壁熄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碳竟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年草丧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莹桅。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昌执,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诈泼,到底是詐尸還是另有隱情懂拾,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布厂汗,位于F島的核電站委粉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏娶桦。R本人自食惡果不足惜贾节,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衷畦。 院中可真熱鬧栗涂,春花似錦、人聲如沸祈争。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忿墅,卻和暖如春扁藕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疚脐。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工亿柑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棍弄。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓望薄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親呼畸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痕支,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容