ZGC 詳解

目錄

  • ZGC簡(jiǎn)介和性能
  • ZGC流程介紹
  • ZGC堆的內(nèi)存布局
  • ZGC對(duì)NUMA支持
  • 顏色指針在ZGC中的運(yùn)用
  • 讀屏障在ZGC中的運(yùn)用
  • strip(條帶)在ZGC中的運(yùn)用
  • ZGC全流程動(dòng)畫(huà)示意

一, ZGC簡(jiǎn)介和性能

G1的目標(biāo)是在可控的停頓時(shí)間內(nèi)完成垃圾回收,所以進(jìn)行了分區(qū)設(shè)計(jì),在回收時(shí)采用部分內(nèi)存回收(在YGC時(shí)會(huì)回收所有新生代分區(qū),在混合回收時(shí)會(huì)回收所有的新生代分區(qū)和部分老生代分區(qū)),支持的內(nèi)存也可以達(dá)到幾十個(gè)GB或者上百個(gè)GB。為了進(jìn)行部分回收,G1實(shí)現(xiàn)了RSet管理對(duì)象的引用關(guān)系?;贕1設(shè)計(jì)上的特點(diǎn),導(dǎo)致存在以下問(wèn)題:

  1. 停頓時(shí)間過(guò)長(zhǎng),通常G1的停頓時(shí)間要達(dá)到幾十到幾百毫秒;這個(gè)數(shù)字其實(shí)已經(jīng)非常小了,但是我們知道垃圾回收發(fā)生導(dǎo)致應(yīng)用程序在這幾十或者幾百毫秒中不能提供服務(wù),在某些場(chǎng)景中,特別是對(duì)用戶體驗(yàn)有較高要求的情況下不能滿足實(shí)際需求。
  2. 內(nèi)存利用率不高,通常引用關(guān)系的處理需要額外消耗內(nèi)存,一般占整個(gè)內(nèi)存的1%~20%左右。
  3. 支持的內(nèi)存空間有限,不適用于超大內(nèi)存的系統(tǒng),特別是在內(nèi)存容量高于100GB的系統(tǒng)中,會(huì)因內(nèi)存過(guò)大而導(dǎo)致停頓時(shí)間增長(zhǎng)。

ZGC作為新一代的垃圾回收器,在設(shè)計(jì)之初就定義了三大目標(biāo):

  1. 支持TB級(jí)內(nèi)存
  2. 停頓時(shí)間控制在10ms之內(nèi)
  3. 對(duì)程序吞吐量影響小于15%。


    image.png

實(shí)際上目前ZGC已經(jīng)滿足設(shè)計(jì)之初定義的目標(biāo),最大支持4TB堆空間,依據(jù)實(shí)際測(cè)試的情況來(lái)看,停頓時(shí)間通常都在10ms以下,并且垃圾回收所引起的暫停時(shí)間并不會(huì)隨著內(nèi)存的增大而延長(zhǎng)。

ZGC如何設(shè)計(jì)以達(dá)成目標(biāo)?
簡(jiǎn)單地說(shuō),就是ZGC把一切能并發(fā)處理的工作都并發(fā)執(zhí)行。
ZGC是在G1的基礎(chǔ)上發(fā)展起來(lái)的,我們知道G1中實(shí)現(xiàn)了并發(fā)標(biāo)記,所以標(biāo)記已經(jīng)不會(huì)再影響停頓時(shí)間了。

G1中的停頓時(shí)間主要來(lái)自垃圾回收(YGC和混合回收)階段中的復(fù)制算法,在復(fù)制算法中,需要把對(duì)象轉(zhuǎn)移到新的空間中,并且更新其他對(duì)象到這個(gè)對(duì)象的引用。實(shí)際中對(duì)象的轉(zhuǎn)移涉及內(nèi)存的分配和對(duì)象成員變量的復(fù)制,而對(duì)象成員變量的復(fù)制是非常耗時(shí)的。

在G1中對(duì)象的轉(zhuǎn)移都是在STW中并行執(zhí)行的,而ZGC就是把對(duì)象的轉(zhuǎn)移也并發(fā)執(zhí)行,從而滿足停頓時(shí)間在10ms以下。

我們看到G1只有在MARKING的時(shí)候,是并發(fā)的。而ZGC 在對(duì)象的復(fù)制和壓縮,復(fù)制集的選擇,等很多方面都改成了并發(fā)(和應(yīng)用線程同時(shí)進(jìn)行)。這就是它STW時(shí)間如此之短的秘訣。


image.png

在最新的JDK版本中,后面2項(xiàng)沒(méi)有打鉤的,ZGC也把他們優(yōu)化進(jìn)并發(fā)步驟中,根據(jù)他們最新的測(cè)試,STW最大停頓時(shí)間可以縮小到1MS。

上圖左邊也展示了一些ZGC里的經(jīng)典特性。比如說(shuō)用到了讀屏障,顏色指針。是個(gè)單代的垃圾收集器(不區(qū)分年輕代和老年代, 在JVMLS 2018上,ZGC的領(lǐng)隊(duì)Per大大明確表示目前的ZGC沒(méi)有分代只是為了實(shí)現(xiàn)簡(jiǎn)單,目前正在考慮給ZGC添加分代支持或者是添加一個(gè)Thread-Local GC來(lái)起到類(lèi)似Young GC的作用,還在探索中),是個(gè)部分壓縮的算法(和G1類(lèi)似,有解決內(nèi)存碎片的標(biāo)記整理步驟)。立即的內(nèi)存重用,和NUMA友好的特性。
我們會(huì)在下文中或多或少的展開(kāi)介紹這些特點(diǎn)。
在JDK11中的ZGC只支持 Linux/x86_64


image.png

現(xiàn)在ZGC的目標(biāo)是1MS,原來(lái)ZGC的停頓時(shí)間和堆SIZE無(wú)關(guān),但是和ROOT-SET的SIZE有關(guān)。他們希望實(shí)現(xiàn)并發(fā)的線程棧掃描,這樣可以使得ZGC的停頓時(shí)間和ROOT-SET SIZE也無(wú)關(guān)??梢园研阅芴嵘?MS的效果。大概和下圖這么牛逼


image.png

當(dāng)然我們?cè)賮?lái)看看ZGC和G1比有多牛逼。


image.png
image.png

在縮短延遲的同時(shí),吞吐率也不受影響。


image.png

image.png

一, ZGC流程介紹

并發(fā)垃圾回收算法實(shí)際上是以復(fù)制算法為基礎(chǔ),增加了并發(fā)處理。我們先回顧一下復(fù)制算法,它可以概括為3個(gè)階段,分別為標(biāo)記(mark)、轉(zhuǎn)移(relocate)和重定位(remap)。這3個(gè)階段分別完成的功能是:

  • 標(biāo)記:從根集合出發(fā),標(biāo)記活躍對(duì)象;此時(shí)內(nèi)存中存在活躍對(duì)象和已死亡對(duì)象。
  • 轉(zhuǎn)移:把活躍對(duì)象轉(zhuǎn)移(復(fù)制)到新的內(nèi)存上,原來(lái)的內(nèi)存空間可以回收。
  • 重定位:因?yàn)閷?duì)象的內(nèi)存地址發(fā)生了變化,所以所有指向?qū)ο罄系刂返闹羔樁家{(diào)整到對(duì)象新的地址上。

從細(xì)節(jié)角度可以分為如下步驟


image.png

1)初始標(biāo)記,從根集合出發(fā),找出根集合直接引用的活躍對(duì)象,并入棧;該步需要STW。
2)并發(fā)標(biāo)記,根據(jù)初始標(biāo)記找到的根對(duì)象,使用深度優(yōu)先遍歷對(duì)象的成員變量進(jìn)行標(biāo)記;并發(fā)標(biāo)記需要解決標(biāo)記過(guò)程中引用關(guān)系變化導(dǎo)致的漏標(biāo)記問(wèn)題
3)再標(biāo)記和非強(qiáng)根并行標(biāo)記,在并發(fā)標(biāo)記結(jié)束后嘗試終結(jié)標(biāo)記動(dòng)作,理論上并發(fā)標(biāo)記結(jié)束后所有待標(biāo)記的對(duì)象會(huì)全部完成,但是因?yàn)镚C工作線程和應(yīng)用程序線程是并發(fā)運(yùn)行,所以可能存在GC工作線程執(zhí)行結(jié)束標(biāo)記時(shí),應(yīng)用程序線程又有新的引用關(guān)系變化導(dǎo)致漏標(biāo)記,所以這一步先判斷是否真的結(jié)束了對(duì)象的標(biāo)記,如果沒(méi)有結(jié)束就還會(huì)啟動(dòng)并行標(biāo)記,所以這一步需要STW。另外,在該步中,還會(huì)對(duì)非強(qiáng)根(軟應(yīng)用,虛引用等)進(jìn)行并行標(biāo)記。
4)并發(fā)處理非強(qiáng)引用和非強(qiáng)根并發(fā)標(biāo)記
5)重置轉(zhuǎn)移集合中的頁(yè)面,實(shí)際上第一次垃圾回收時(shí)無(wú)須處理這一步。
6)回收無(wú)效的頁(yè)面,實(shí)際上在內(nèi)存充足的情況下不會(huì)觸發(fā)這一步。
7)并發(fā)選擇對(duì)象的轉(zhuǎn)移集合,轉(zhuǎn)移集合中就是待回收的頁(yè)面。
8)并發(fā)初始化轉(zhuǎn)移集合中的每個(gè)頁(yè)面,在后續(xù)重定位(也稱(chēng)為Remap)時(shí)需要的對(duì)象轉(zhuǎn)移表(Forward Table)就是在這一步初始化的。
9)轉(zhuǎn)移根對(duì)象引用的對(duì)象,該步需要STW。
10)并發(fā)轉(zhuǎn)移,把對(duì)象移動(dòng)到新的頁(yè)面中,這樣對(duì)象所在的老的頁(yè)面中所有活躍對(duì)象都被轉(zhuǎn)移了,頁(yè)面就可以被回收重用。

image.png

為了畫(huà)圖方便,把步驟5)~步驟8)放在一個(gè)并發(fā)步驟中,實(shí)際中這是4步,并且這4步是串行執(zhí)行,每一步都是并發(fā)執(zhí)行的。

上述步驟,你可能會(huì)看暈。我們來(lái)看簡(jiǎn)單的版本。


image.png

上述3個(gè)藍(lán)線代表需要STW, 灰線代表可以并發(fā)運(yùn)作。

  1. 根集合標(biāo)記(STW)
  2. 并發(fā)標(biāo)記
  3. 并發(fā)標(biāo)記的同步點(diǎn)(STW,同G1)還會(huì)處理一些非強(qiáng)根
  4. 并發(fā)-轉(zhuǎn)移前的準(zhǔn)備(上面的4-8步,最核心的是引用處理,非強(qiáng)根清理,轉(zhuǎn)移集(relocation set)的選擇)
  5. 轉(zhuǎn)移 在轉(zhuǎn)移集中的根對(duì)象(STW)
  6. 并發(fā)轉(zhuǎn)移其他在轉(zhuǎn)移集中的對(duì)象

并發(fā)算法中明確地提到重定位階段,但上面的步驟中并沒(méi)有體現(xiàn)。在ZGC中并沒(méi)有明確這一步,重定位實(shí)際上被合并到標(biāo)記階段中,即在標(biāo)記的時(shí)候如果發(fā)現(xiàn)對(duì)象引用到老的地址,這時(shí)會(huì)先完成重定位更新對(duì)象的引用關(guān)系,然后再標(biāo)記對(duì)象。所以實(shí)質(zhì)上ZGC的并發(fā)垃圾回收中還是包含了重定位這一階段,只不過(guò)重定位和標(biāo)記階段復(fù)用了。

image.png

思考題. 有3個(gè)步驟需要STW的原因是什么?他們和應(yīng)用線程并發(fā)有什么問(wèn)題?

第二個(gè)STW,如果你看過(guò)我的G1的文章,就很好理解。如果不STW,那么應(yīng)用程序一直在改引用,會(huì)找不到一個(gè)時(shí)間點(diǎn),把待標(biāo)記對(duì)象在QUEUE中給全部排干凈。那么會(huì)一直處于并發(fā)標(biāo)記階段。
第三個(gè)STW,在初始轉(zhuǎn)移中所做的工作主要針對(duì)根集合引用的對(duì)象,如果這些對(duì)象所在的頁(yè)面在轉(zhuǎn)移集中,則轉(zhuǎn)移這些對(duì)象;如果對(duì)象所在的頁(yè)面不在轉(zhuǎn)移集中,則直接調(diào)整對(duì)象的頁(yè)面映射視圖。第10步中的并發(fā)標(biāo)記是對(duì)所有在轉(zhuǎn)移集的頁(yè)面中所有活躍對(duì)象做轉(zhuǎn)移,在并發(fā)轉(zhuǎn)移之后的下一次垃圾回收的標(biāo)記階段完成重定位。那么我們能不能把第9步的工作分散在第10步和下一次垃圾回收的標(biāo)記階段進(jìn)行?
要回答這個(gè)問(wèn)題,需要理解讀屏障,和對(duì)ZGC的全貌有一個(gè)認(rèn)識(shí),我會(huì)放在文章最后解答。
關(guān)于第一個(gè)STW,也要對(duì)ZGC的工作原理有一定的理解。我會(huì)在講完(顏色指針怎么工作后解答這個(gè)問(wèn)題)

三, ZGC堆的內(nèi)存布局

ZGC 支持的最大堆內(nèi)存為4T。 但是我們確需要保留更大的虛擬地址空間。如下圖


image.png

因?yàn)閆GC里需要用到3個(gè)地址視圖。
分別是Marked0、Marked1和Remapped,而且有趣的是這3個(gè)視圖會(huì)映射到操作系統(tǒng)的同一物理地址。這就是ZGC中Color Pointers的概念,通過(guò)Color Pointers來(lái)區(qū)別不同的虛擬視圖。
在ZGC中常見(jiàn)的幾個(gè)虛擬空間有[0 ~ 4TB)、[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)。其中[0 ~ 4TB)對(duì)應(yīng)的是Java的堆空間;[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)分別對(duì)應(yīng)Marked0、Marked1和Remapped這3個(gè)視圖。這幾個(gè)區(qū)域有什么關(guān)系?我們先看圖
mutator 就是應(yīng)用線程的意思


image.png

0~4TB的虛擬地址是ZGC提供給應(yīng)用程序使用的虛擬空間,它并不會(huì)映射到真正的物理地址。
·操作系統(tǒng)管理的虛擬內(nèi)存為Marked0、Marked1和Remapped這3個(gè)空間,且它們對(duì)應(yīng)同一物理空間。

·在ZGC中這3個(gè)空間在同一時(shí)間點(diǎn)有且僅有一個(gè)空間有效。為什么這么設(shè)計(jì)?這是利用虛擬空間換時(shí)間;這3個(gè)空間的切換是由垃圾回收的不同階段觸發(fā)的。(下文介紹顏色指針會(huì)介紹)

應(yīng)用程序可見(jiàn)并使用的虛擬地址為0~4TB,經(jīng)ZGC轉(zhuǎn)化,真正使用的虛擬地址為[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB),操作系統(tǒng)管理的虛擬地址也是[4TB ~ 8TB)、[8TB ~ 12TB)和[16TB ~ 20TB)。應(yīng)用程序可見(jiàn)的虛擬地址[0 ~ 4TB)和物理內(nèi)存直接的關(guān)聯(lián)由ZGC來(lái)管理。

image.png

為了細(xì)粒度地控制內(nèi)存的分配,和G1一樣,ZGC將內(nèi)存劃分成小的分區(qū),在ZGC中稱(chēng)為頁(yè)面(page)。ZGC支持3種頁(yè)面,分別為小頁(yè)面、中頁(yè)面和大頁(yè)面。其中小頁(yè)面指的是2MB的頁(yè)面空間,中頁(yè)面指32MB的頁(yè)面空間,大頁(yè)面指受操作系統(tǒng)控制的大頁(yè)。我們先回顧一下操作系統(tǒng)所支持的大頁(yè)。

標(biāo)準(zhǔn)大頁(yè)(huge page)是Linux Kernel 2.6引入的,目的是通過(guò)使用大頁(yè)內(nèi)存來(lái)取代傳統(tǒng)的4KB內(nèi)存頁(yè)面,以適應(yīng)越來(lái)越大的系統(tǒng)內(nèi)存,讓操作系統(tǒng)可以支持現(xiàn)代硬件架構(gòu)的大頁(yè)面容量功能。它有兩種大小:2MB和1GB。2MB頁(yè)塊大小適合用于吉字節(jié)級(jí)的內(nèi)存,1GB頁(yè)塊大小適合用于太字節(jié)級(jí)別的內(nèi)存;2MB是默認(rèn)的大頁(yè)尺寸。

一個(gè)ZGC的頁(yè)面可能由幾個(gè)不連續(xù)的操作系統(tǒng)頁(yè)面組成。

image.png

ZGC對(duì)于不同頁(yè)面回收的策略也不同。簡(jiǎn)單地說(shuō),小頁(yè)面優(yōu)先回收;中頁(yè)面和大頁(yè)面則盡量不回收。


image.png

四, ZGC對(duì)NUMA支持

在過(guò)去,對(duì)于X86架構(gòu)的計(jì)算機(jī),內(nèi)存控制器還沒(méi)有整合進(jìn)CPU,所有對(duì)內(nèi)存的訪問(wèn)都需要通過(guò)北橋芯片來(lái)完成。X86系統(tǒng)中的所有內(nèi)存都可以通過(guò)CPU進(jìn)行同等訪問(wèn)。任何CPU訪問(wèn)任何內(nèi)存的速度是一致的,不必考慮不同內(nèi)存地址之間的差異,這稱(chēng)為“統(tǒng)一內(nèi)存訪問(wèn)”(Uniform Memory Access,UMA)。UMA系統(tǒng)的架構(gòu)示意圖如圖所示。


image.png

在UMA中,各處理器與內(nèi)存單元通過(guò)互聯(lián)總線進(jìn)行連接,各個(gè)CPU之間沒(méi)有主從關(guān)系。之后的X86平臺(tái)經(jīng)歷了一場(chǎng)從“拼頻率”到“拼核心數(shù)”的轉(zhuǎn)變,越來(lái)越多的核心被盡可能地塞進(jìn)了同一塊芯片上,各個(gè)核心對(duì)于內(nèi)存帶寬的爭(zhēng)搶訪問(wèn)成為瓶頸,所以人們希望能夠把CPU和內(nèi)存集成在一個(gè)單元上(稱(chēng)為Socket),這就是非統(tǒng)一內(nèi)存訪問(wèn)(Non-Uniform Memory Access,NUMA)。很明顯,在NUMA下,CPU訪問(wèn)本地存儲(chǔ)器的速度比訪問(wèn)非本地存儲(chǔ)器快一些。下圖所示是初期處理器架構(gòu)示意圖。


image.png

ZGC是支持NUMA的,在進(jìn)行小頁(yè)面分配時(shí)會(huì)優(yōu)先從本地內(nèi)存分配,當(dāng)不能分配時(shí)才會(huì)從遠(yuǎn)端的內(nèi)存分配。對(duì)于中頁(yè)面和大頁(yè)面的分配,ZGC并沒(méi)有要求從本地內(nèi)存分配,而是直接交給操作系統(tǒng),由操作系統(tǒng)找到一塊能滿足ZGC頁(yè)面的空間。
ZGC這樣設(shè)計(jì)的目的在于,對(duì)于小頁(yè)面,存放的都是小對(duì)象,從本地內(nèi)存分配速度很快,且不會(huì)造成內(nèi)存使用的不平衡,而中頁(yè)面和大頁(yè)面因?yàn)樾枰目臻g大,如果也優(yōu)先從本地內(nèi)存分配,極易造成內(nèi)存使用不均衡,反而影響性能。

五, 顏色指針在ZGC中的運(yùn)用

image.png

顏色指針可以說(shuō)是ZGC的核心概念。因?yàn)樗谥羔樦薪枇藥讉€(gè)位出來(lái)做事情,所以它必須要求在64位的機(jī)器上才可以工作。并且因?yàn)橐?4位的指針,也就不能支持壓縮指針。(關(guān)于JVM的壓縮指針,可以自行百度其他文章)

ZGC支持64位系統(tǒng),我們看一下ZGC是如何使用64位地址的。ZGC中低42位(第0 ~ 41位)用于描述真正的虛擬地址(這就是上面提到的應(yīng)用程序可以使用的堆空間),接著的4位(第42 ~ 45位)用于描述元數(shù)據(jù),其實(shí)就是大家所說(shuō)的Color Pointers,還有1位(第46位)目前暫時(shí)沒(méi)有使用,最高17位(第47~63位)固定為0


image.png
image.png

由于42位地址最大的尋址空間就是4TB,這就是ZGC一直宣稱(chēng)自己最大支持4TB內(nèi)存的原因。這里還有視圖的概念,Marked0、Marked1和Remapped就是3個(gè)視圖,分別將第42、43、44位設(shè)置為1,就表示采用對(duì)應(yīng)的視圖。在ZGC中,這4位標(biāo)記位的目的并不是用于地址尋址的,而是為了區(qū)分Marked0、Marked1和Remapped這3個(gè)視圖。當(dāng)然對(duì)于操作系統(tǒng)來(lái)說(shuō),這4位標(biāo)記位代表了不同的虛擬地址空間,而這些不同標(biāo)記位指示的不同虛擬空間通過(guò)mmap映射在同一物理地址;顏色指針能夠快速實(shí)現(xiàn)并發(fā)標(biāo)記、轉(zhuǎn)移和重定位。

為什么最高位16個(gè)不能用?

由于X86_64處理器硬件的限制,目前X86_64處理器地址線只有48條,也就是說(shuō)64位系統(tǒng)支持的地址空間為256TB。為什么處理器的指令集是64位的,但是硬件僅支持48位的地址?最主要的原因是成本問(wèn)題,即便到目前為止由48位地址訪問(wèn)的256TB的內(nèi)存空間也是非常巨大的,也沒(méi)有多少系統(tǒng)有這么大的內(nèi)存,所以在設(shè)計(jì)CPU時(shí)僅僅支持48位地址,可以少用很多硬件

ZGC基于顏色指針的并發(fā)處理算法

ZGC初始化之后,整個(gè)內(nèi)存空間的地址視圖被設(shè)置為Remapped,當(dāng)進(jìn)入標(biāo)記階段時(shí)的視圖轉(zhuǎn)變?yōu)镸arked0(也稱(chēng)為M0)或者M(jìn)arked1(也稱(chēng)為M1),從標(biāo)記階段結(jié)束進(jìn)入轉(zhuǎn)移階段時(shí)的視圖再次設(shè)置為Remapped。ZGC通過(guò)視圖的切換加上SATB算法實(shí)現(xiàn)并發(fā)處理。具體算法如下。
1.初始化階段
在ZGC初始化之后,此時(shí)地址視圖為Remapped,程序正常運(yùn)行,在內(nèi)存中分配對(duì)象,滿足一定條件后垃圾回收啟動(dòng)
2.標(biāo)記階段
第一次進(jìn)入標(biāo)記階段時(shí)視圖為M0,在標(biāo)記階段,應(yīng)用程序和標(biāo)記線程并發(fā)執(zhí)行,那么對(duì)象的訪問(wèn)可能來(lái)自標(biāo)記線程和應(yīng)用程序線程。

  • 標(biāo)記線程:它從根集合開(kāi)始標(biāo)記對(duì)象,在標(biāo)記前先判斷對(duì)象的地址視圖,如果發(fā)現(xiàn)對(duì)象的地址視圖是M0,說(shuō)明對(duì)象是在進(jìn)入標(biāo)記階段之后新分配的對(duì)象或者對(duì)象已經(jīng)完成了標(biāo)記(對(duì)象活躍),無(wú)須處理。如果發(fā)現(xiàn)對(duì)象的地址視圖是Remapped,說(shuō)明對(duì)象是前一階段分配的,而且通過(guò)根集合可達(dá),所以把對(duì)象的地址視圖從Remapped調(diào)整為M0。(M0表示活躍)
  • 應(yīng)用程序線程如果創(chuàng)建新的對(duì)象,則對(duì)象的地址視圖為M0。
    如果應(yīng)用程序線程訪問(wèn)對(duì)象并且對(duì)象的地址視圖是Remapped,說(shuō)明對(duì)象是前一階段分配的,按照SATB的算法,只要把該對(duì)象的視圖調(diào)整為M0就能防止對(duì)象漏標(biāo)。只標(biāo)記應(yīng)用線程訪問(wèn)到的對(duì)象還不夠,實(shí)際上還需要把對(duì)象的成員變量所引用的對(duì)象都進(jìn)行遞歸標(biāo)記。如果應(yīng)用線程訪問(wèn)對(duì)象地址視圖是M0,說(shuō)明對(duì)象是在進(jìn)入標(biāo)記階段之后新分配的對(duì)象或者對(duì)象已經(jīng)完成了標(biāo)記,無(wú)須額外處理,直接訪問(wèn)。

所以,在標(biāo)記階段結(jié)束之后,對(duì)象的地址視圖要么是M0(活躍),要么是Remapped(垃圾)。這里的虛擬地址雖然不一樣,但是指向的是物理內(nèi)存的同一個(gè)區(qū)域
所有標(biāo)記為M0的對(duì)象放入活躍信息表

3.并發(fā)轉(zhuǎn)移階段
標(biāo)記結(jié)束后就進(jìn)入轉(zhuǎn)移階段,此時(shí)地址視圖再次被設(shè)置為Remapped。

轉(zhuǎn)移階段會(huì)把活躍對(duì)象轉(zhuǎn)移到新的內(nèi)存中,并回收對(duì)象轉(zhuǎn)移前的內(nèi)存空間。在轉(zhuǎn)移階段,應(yīng)用程序和標(biāo)記線程并發(fā)執(zhí)行,那么對(duì)象的訪問(wèn)可能來(lái)自轉(zhuǎn)移線程和應(yīng)用程序線程。

  • 轉(zhuǎn)移線程:轉(zhuǎn)移線程僅僅根據(jù)活躍對(duì)象進(jìn)行轉(zhuǎn)移。當(dāng)轉(zhuǎn)移線程訪問(wèn)對(duì)象時(shí):
    如果對(duì)象在對(duì)象活躍信息表中并且視圖為M0,則轉(zhuǎn)移對(duì)象,并且視圖從M0調(diào)整為Remapped。
    如果對(duì)象在對(duì)象活躍信息表中并且視圖Remapped,說(shuō)明對(duì)象已經(jīng)被轉(zhuǎn)移,無(wú)須處理。

  • 應(yīng)用程序線程如果創(chuàng)建新的對(duì)象,則對(duì)象的地址視圖為Remapped。
    如果應(yīng)用線程訪問(wèn)對(duì)象且不在活躍信息表中,則說(shuō)明是新創(chuàng)建的或者對(duì)象無(wú)須轉(zhuǎn)移,無(wú)須處理。
    如果應(yīng)用線程訪問(wèn)對(duì)象且在活躍信息表中且視圖為Remapped,說(shuō)明對(duì)象已經(jīng)被轉(zhuǎn)移,無(wú)須處理。
    如果應(yīng)用程序線程訪問(wèn)在對(duì)象活躍信息表中,且視圖為M0,說(shuō)明對(duì)象是標(biāo)記階段標(biāo)記的活躍對(duì)象,所以需要轉(zhuǎn)移對(duì)象
    在對(duì)象轉(zhuǎn)移以后,對(duì)象的地址視圖從M0調(diào)整為Remapped;
    注意,只把應(yīng)用線程讀到的對(duì)象進(jìn)行轉(zhuǎn)移還不夠,實(shí)際上還需要把對(duì)象的成員變量所引用的對(duì)象都進(jìn)行轉(zhuǎn)移,ZGC對(duì)這一實(shí)現(xiàn)做了優(yōu)化,由轉(zhuǎn)移線程完成對(duì)象成員變量的轉(zhuǎn)移。
    至此,ZGC的一個(gè)垃圾回收周期中,并發(fā)標(biāo)記和并發(fā)轉(zhuǎn)移就結(jié)束了。

我們提到在標(biāo)記階段存在兩個(gè)地址視圖M0和M1,上面的算法過(guò)程顯示只用到了一個(gè)地址視圖,為什么設(shè)計(jì)成兩個(gè)?簡(jiǎn)單地說(shuō)是為了區(qū)別前一次標(biāo)記和當(dāng)前標(biāo)記。

第一次垃圾回收時(shí)地址視圖為M0,假設(shè)標(biāo)記了兩個(gè)對(duì)象ObjA和ObjB,說(shuō)明ObjA和ObjB都是活躍的,它們的地址視圖都是M0。在轉(zhuǎn)移階段,ZGC是按照頁(yè)面進(jìn)行部分內(nèi)存垃圾回收的,也就是說(shuō)當(dāng)對(duì)象所在的頁(yè)面需要回收時(shí),頁(yè)面里面的對(duì)象需要被轉(zhuǎn)移,如果頁(yè)面不需要轉(zhuǎn)移,頁(yè)面里面的對(duì)象也就不需要轉(zhuǎn)移。

假設(shè)ObjA所在的頁(yè)面被回收,ObjB所在的頁(yè)面在這一次垃圾回收中不會(huì)被回收。ObjA被轉(zhuǎn)移后,它的地址視圖從M0調(diào)整為Remapped,ObjB不會(huì)被轉(zhuǎn)移,ObjB的地址視圖仍然為M0。

那么下一次垃圾回收標(biāo)記階段開(kāi)始的時(shí)候,存在兩種地址視圖的對(duì)象

  1. 地址視圖為Remapped的對(duì)象,說(shuō)明該對(duì)象在并發(fā)轉(zhuǎn)移階段被轉(zhuǎn)移或者被訪問(wèn)過(guò);
  2. 地址視圖為M0的對(duì)象,說(shuō)明該對(duì)象在前一次垃圾回收的標(biāo)記階段已經(jīng)被標(biāo)記。

如果本次垃圾回收標(biāo)記階段仍然使用M0這個(gè)地址視圖,那么就不能區(qū)分出對(duì)象是活躍的,還是上一次垃圾回收標(biāo)記過(guò)的。

所以新標(biāo)記階段使用了另外一個(gè)地址視圖M1,則標(biāo)記結(jié)束后所有活躍對(duì)象的地址視圖都為M1。
此時(shí)這3個(gè)地址視圖代表的含義是:

  • M1:本次垃圾回收中識(shí)別的活躍對(duì)象。
  • M0:前一次垃圾回收的標(biāo)記階段被標(biāo)記過(guò)的活躍對(duì)象,對(duì)象在轉(zhuǎn)移階段未被轉(zhuǎn)移,但是在本次垃圾回收中被識(shí)別為不活躍對(duì)象。
  • Remapped:前一次垃圾回收的轉(zhuǎn)移階段發(fā)生轉(zhuǎn)移的對(duì)象或者是被應(yīng)用程序線程訪問(wèn)的對(duì)象,但是在本次垃圾回收中被識(shí)別為不活躍對(duì)象。

上述過(guò)程算法演示

image.png
image.png

image.png

image.png

六, 讀屏障在ZGC中的運(yùn)用

image.png

image.png

image.png

image.png

對(duì)應(yīng)用程序線程進(jìn)行標(biāo)記發(fā)生在讀對(duì)象時(shí),為了觸發(fā)標(biāo)記動(dòng)作可以設(shè)計(jì)一個(gè)讀屏障,在字節(jié)碼層面或者編譯代碼層面給讀操作增加一個(gè)額外的處理即可。

讀屏障由讀命令觸發(fā),JVM有3種運(yùn)行狀態(tài):解釋執(zhí)行、C1優(yōu)化執(zhí)行和C2優(yōu)化執(zhí)行。不同的運(yùn)行狀態(tài),讀屏障的觸發(fā)代碼略有不同,但它們使用的讀屏障是完全一樣的。

我們從最簡(jiǎn)單的解釋執(zhí)行看一下讀屏障的實(shí)現(xiàn)。讀屏障在解釋執(zhí)行時(shí)通過(guò)load相關(guān)的字節(jié)碼指令加載數(shù)據(jù),我們直接從堆空間中加載對(duì)象的地方了解一下讀屏障,其代碼如下:

template <DecoratorSet decorators, typename BarrierSetT>
template <typename T>
inline oop ZBarrierSet::AccessBarrier<decorators,BarrierSetT>::oop_load_in_heap(T* addr) {
  verify_decorators_absent<ON_UNKNOWN_OOP_REF>();
  const oop o = Raw::oop_load_in_heap(addr);
  return load_barrier_on_oop_field_preloaded(addr, o);
}

這里調(diào)用的load_barrier_on_oop_field_preloaded就是讀屏障,在對(duì)象加載完成后做額外的處理。


image.png

讀屏障增加了額外的代碼,所以會(huì)引起性能下降。據(jù)Per Linda的介紹,SPECjbb測(cè)試表明使用讀屏障之后,性能大概降低4%左右。

image.png

上面提及了標(biāo)記的一些基本概念,STW的時(shí)候是并行的(多個(gè)標(biāo)記線程一起標(biāo)記ROOT直接引用的對(duì)象),之后是并發(fā)的。讀屏障可以讓?xiě)?yīng)用線程再使用對(duì)象的時(shí)候,知道它是不是一個(gè)GOOD COLOR的對(duì)象,如果不是。就先要幫他恢復(fù)成GOOD COLOR。
如果在標(biāo)記的時(shí)刻,地址視圖為M0,發(fā)現(xiàn)他不是M0,那么應(yīng)用線程會(huì)幫助把它(如果需要轉(zhuǎn)移還會(huì)幫助轉(zhuǎn)移)標(biāo)記為M0,因?yàn)槲铱梢栽L問(wèn)到它所以它是活的。
上述的做法通過(guò)任意一次讀操作,都把顏色置為灰色(活躍),從而打破并發(fā)標(biāo)記中漏標(biāo)的充要條件(充要條件參考我的G1 詳解

回答思考題3

為什么初始標(biāo)記需要STW?
假如我們不STW,那么應(yīng)用程序和標(biāo)記線程并發(fā)。CPU可以在任意位置切換。假設(shè)現(xiàn)在是初始狀態(tài),地址視圖為REMAPPED。應(yīng)用線程從棧中的引用讀到一個(gè)堆中的對(duì)象OBJ_A。并且要把它的引用給斷開(kāi)。

local1.next = obj_a

按照STAB的設(shè)計(jì),如果此時(shí)已經(jīng)開(kāi)始并發(fā)標(biāo)記了,那么OBJ_A會(huì)標(biāo)記為M0。但是此時(shí)還沒(méi)開(kāi)始。所以應(yīng)用程序把OBj_A讀出來(lái)。準(zhǔn)備給LOCAL1.NEXT賦值前,切換到了標(biāo)記線程。
標(biāo)記線程啟動(dòng),把地址視圖改為M0。然后從LOCAL1這個(gè)跟開(kāi)始掃描。發(fā)現(xiàn)LOCAL1.NEXT=NULL。那么把LOCAL1標(biāo)記為M0就結(jié)束了。
切回到應(yīng)用線程,此時(shí)繼續(xù)完成賦值任務(wù)。就造成了一個(gè)黑色對(duì)象指向了一個(gè)白色對(duì)象的漏標(biāo)問(wèn)題。白色對(duì)象會(huì)被GC掉之后,程序會(huì)出錯(cuò)。
為了解決這個(gè)問(wèn)題,程序會(huì)利用STW的安全點(diǎn),來(lái)防止這種應(yīng)用線程做到一半的操作被切走的情況。

七, strip(條帶)在ZGC中的運(yùn)用

ZGC中引入了標(biāo)記條帶(mark strip)。為了讓線程之間標(biāo)記的時(shí)候可以互不干擾,減少競(jìng)爭(zhēng)鎖的開(kāi)銷(xiāo)。


image.png

標(biāo)記棧在ZGC中的底層實(shí)現(xiàn)使用的是數(shù)組,因?yàn)闃?biāo)記棧只有一個(gè),所有的并發(fā)標(biāo)記線程會(huì)訪問(wèn)這一個(gè)標(biāo)記棧,所以自然會(huì)想到將這個(gè)標(biāo)記棧進(jìn)行劃分,劃分后形成多個(gè)標(biāo)記條帶,然后讓多個(gè)并發(fā)標(biāo)記線程并行地訪問(wèn)其中的標(biāo)記條帶,互不干擾。例如,Thread0標(biāo)記Strip0中的對(duì)象,而Strip0可能包含多個(gè)內(nèi)存區(qū)域塊。示意圖如下圖所示。


image.png

劃分成多個(gè)標(biāo)記條帶和為并發(fā)工作線程設(shè)置相應(yīng)的標(biāo)記條帶需要提前完成,這樣才能在標(biāo)記時(shí)把待標(biāo)記對(duì)象直接放入相應(yīng)的標(biāo)記條帶中,這是第一步開(kāi)始標(biāo)記做的工作。

ZGC是根據(jù)對(duì)象的地址計(jì)算對(duì)象屬于哪個(gè)標(biāo)記條帶,把地址通過(guò)哈希函數(shù)計(jì)算得到的值作為標(biāo)記條帶號(hào)。

這里其實(shí)有一個(gè)問(wèn)題——在對(duì)象放入標(biāo)記條帶中存在并發(fā)問(wèn)題。設(shè)想這樣一種情況,有兩個(gè)線程T1和T2,有兩個(gè)標(biāo)記條帶Strip1和Strip2,里面分別存放了Obj1和Obj2。T1標(biāo)記Strip1里面的對(duì)象,T2標(biāo)記Strip2里面的對(duì)象。當(dāng)T1標(biāo)記Obj1的時(shí)候,需要把Obj1所有的成員變量進(jìn)行標(biāo)記,假設(shè)發(fā)現(xiàn)Obj1的成員變量按照哈希函數(shù)計(jì)算后需要放入Strip2中,那么T1會(huì)訪問(wèn)Strip2,把該對(duì)象的成員變量指向的待標(biāo)記對(duì)象入棧。
同理,線程T2也可能訪問(wèn)Strip1。
這和我們前面提到的設(shè)計(jì)標(biāo)記條帶的目的完全不同。設(shè)計(jì)標(biāo)記條帶的目的是希望線程T1只訪問(wèn)Strip1,線程T2只訪問(wèn)Strip2,從而完全解決并發(fā)性問(wèn)題。

為了盡可能地讓標(biāo)記線程之間進(jìn)行并發(fā)標(biāo)記,ZGC對(duì)每個(gè)標(biāo)記線程使ZMarkThreadLocalStacks保存需要遍歷的對(duì)象,當(dāng)線程的本地標(biāo)記棧滿時(shí),再把標(biāo)記棧轉(zhuǎn)入ZMarkStripSet中。


image.png

ZMarkStripSet中標(biāo)記條帶是鏈表的形式,所以放入的時(shí)候相當(dāng)簡(jiǎn)單,直接插入新的節(jié)點(diǎn)到鏈表中,而無(wú)須分配額外的空間。

對(duì)于并發(fā)工作線程來(lái)說(shuō),所有的標(biāo)記條帶都沒(méi)有對(duì)象,說(shuō)明并發(fā)工作線程可以結(jié)束工作。因?yàn)椴l(fā)工作線程是多線程執(zhí)行,所以判斷的條件是所有工作線程訪問(wèn)的條帶都沒(méi)有對(duì)象。對(duì)于多線程來(lái)說(shuō),可能存在有些線程待標(biāo)記對(duì)象少,執(zhí)行得快,而有些線程待標(biāo)記對(duì)象多,執(zhí)行得慢的情況,所以并發(fā)執(zhí)行中需要有一種機(jī)制來(lái)進(jìn)行負(fù)載均衡,讓所有并發(fā)工作線程盡可能同時(shí)結(jié)束。并發(fā)標(biāo)記工作線程的負(fù)載均衡是通過(guò)竊取其他線程的任務(wù)完成的,即當(dāng)本線程沒(méi)有可以執(zhí)行的任務(wù)時(shí),并不會(huì)立即停止線程,而是先從其他的線程竊取任務(wù),然后執(zhí)行任務(wù)。

image.png

因?yàn)閼?yīng)用程序線程也可能執(zhí)行標(biāo)記,而且應(yīng)用程序線程標(biāo)記后,待標(biāo)記對(duì)象存放在應(yīng)用程序線程的本地標(biāo)記條帶中,所以當(dāng)并發(fā)工作線程結(jié)束標(biāo)記任務(wù)后,應(yīng)該將應(yīng)用程序線程中的待標(biāo)記對(duì)象轉(zhuǎn)移到并發(fā)工作線程的標(biāo)記條帶中,讓并發(fā)標(biāo)記線程繼續(xù)高速工作。因此在并發(fā)標(biāo)記結(jié)束過(guò)程中設(shè)計(jì)了主動(dòng)刷新機(jī)制,并發(fā)標(biāo)記0號(hào)線程把應(yīng)用程序線程中的待標(biāo)記對(duì)象刷新到并發(fā)線程的標(biāo)記條帶中。繼續(xù)工作。這就要求應(yīng)用程序需要能夠進(jìn)入到自己的安全點(diǎn)再去響應(yīng)主動(dòng)刷新,對(duì)于單個(gè)線程走進(jìn)安全點(diǎn)暫停做別的事情,需要支持HandShake機(jī)制。

JDK 10中引入JEP312 Thread-Local HandShake,該項(xiàng)目實(shí)現(xiàn)單個(gè)線程的暫停,而不是暫停所有線程。HandShake機(jī)制也是通過(guò)VMThread機(jī)制完成的,只不過(guò)HandShake中指定了一個(gè)暫停的目標(biāo)線程。

并發(fā)轉(zhuǎn)移

image.png

因?yàn)槭遣l(fā)轉(zhuǎn)移,所以必然會(huì)涉及應(yīng)用程序線程在轉(zhuǎn)移時(shí)訪問(wèn)待轉(zhuǎn)移的對(duì)象。此時(shí)應(yīng)用程序線程會(huì)先完成轉(zhuǎn)移的任務(wù),然后再訪問(wèn)對(duì)象,這就涉及我們之前介紹的讀屏障。讀屏障的流程圖中,會(huì)根據(jù)頁(yè)面的狀態(tài)判斷進(jìn)行轉(zhuǎn)移還是標(biāo)記或者重定位操作。

思考題2的解答

為什么需要初始轉(zhuǎn)移呢?反正有讀屏障不能直接并發(fā)轉(zhuǎn)移嗎?

應(yīng)用程序線程正在訪問(wèn)對(duì)象,在第10步并發(fā)轉(zhuǎn)移中,應(yīng)用程序線程的訪問(wèn)是需要讀屏障的,此時(shí)在讀屏障中會(huì)把對(duì)象轉(zhuǎn)移到新的地址。
聽(tīng)起來(lái)似乎可行,但是這里有一個(gè)問(wèn)題,我們提到如果應(yīng)用程序線程正在訪問(wèn)對(duì)象,通過(guò)讀屏障完成轉(zhuǎn)移。這個(gè)讀屏障只能在第10步中發(fā)生,所以它針對(duì)的都是在第10步中從根集合中新產(chǎn)生的引用對(duì)象,這些新的對(duì)象可以通過(guò)讀屏障得到正確的處理(即新產(chǎn)生的對(duì)象被正確地轉(zhuǎn)移),但是可能存在這樣一種情況,在進(jìn)入第10步之前已經(jīng)通過(guò)根集合訪問(wèn)了對(duì)象,這時(shí)進(jìn)入第10步后,第10步中的讀屏障對(duì)于這些已經(jīng)訪問(wèn)的對(duì)象就不起作用了,單從轉(zhuǎn)移角度來(lái)說(shuō),對(duì)象仍然可以被并發(fā)轉(zhuǎn)移線程正確地轉(zhuǎn)移。但是從訪問(wèn)角度就會(huì)出現(xiàn)問(wèn)題,此時(shí)如果有新的應(yīng)用程序線程也訪問(wèn)這個(gè)對(duì)象,如果對(duì)象已經(jīng)被轉(zhuǎn)移,那么這個(gè)新應(yīng)用程序線程通過(guò)讀屏障訪問(wèn)到新對(duì)象,如果對(duì)象還未轉(zhuǎn)移,那么這個(gè)新應(yīng)用程序線程則會(huì)通過(guò)讀屏障先轉(zhuǎn)移對(duì)象再訪問(wèn),結(jié)論就是兩個(gè)應(yīng)用程序線程一個(gè)訪問(wèn)老對(duì)象,一個(gè)訪問(wèn)新對(duì)象,如果兩個(gè)應(yīng)用程序線程都對(duì)對(duì)象進(jìn)行修改,就會(huì)發(fā)生數(shù)據(jù)不一致,導(dǎo)致錯(cuò)誤。這就是初始轉(zhuǎn)移STW解決的問(wèn)題。

八, ZGC全流程動(dòng)畫(huà)演示

在JVM啟動(dòng)后和垃圾回收發(fā)生之前,相關(guān)的地址視圖會(huì)被設(shè)置為Remapped。假定應(yīng)用程序運(yùn)行一段時(shí)間后,整個(gè)內(nèi)存的對(duì)象關(guān)系如圖


image.png

圖中對(duì)象1 ~ 5位于小頁(yè)面中,對(duì)象6 ~ 11位于中頁(yè)面中。小頁(yè)面位于虛擬地址的頭部,中頁(yè)面和大頁(yè)面位于虛擬地址的尾部,本例中假設(shè)不存在大頁(yè)面對(duì)象。小頁(yè)面占用的空間為2MB,中頁(yè)面占用的空間為32MB

進(jìn)入初始標(biāo)記后,地址視圖切換為M0,然后從根集合出發(fā),開(kāi)始遍歷直接引用的對(duì)象。
為了更好地突出圖中的變化,統(tǒng)一定義虛線表示正在變化的引用關(guān)系,標(biāo)記過(guò)程中的活躍對(duì)象使用深色背景。初始標(biāo)記結(jié)束后,整個(gè)內(nèi)存的對(duì)象關(guān)系如圖

image.png

對(duì)象1、2和4都被標(biāo)記為活躍的。這里活躍的意思是對(duì)象1、2和4的地址視圖都變成M0。
同時(shí)在初始標(biāo)記結(jié)束后,在標(biāo)記條帶中存在指向?qū)ο?、2和4的指針,標(biāo)記條帶將被用于并發(fā)標(biāo)記。假設(shè)并發(fā)工作線程為兩個(gè),對(duì)象1、2和4將被放在不同的標(biāo)記條帶中,分別由線程1和線程2并發(fā)地標(biāo)記。


image.png

并發(fā)標(biāo)記時(shí),從標(biāo)記條帶獲取對(duì)象,開(kāi)始標(biāo)記。
注意,在圖中初始標(biāo)記中僅僅把對(duì)象1、2和4的地址從Remapped變成了M0,但是并沒(méi)有記錄它們所在頁(yè)面的活躍對(duì)象的信息。
并發(fā)標(biāo)記從對(duì)象1、2和4遍歷對(duì)象的成員變量,同時(shí)統(tǒng)計(jì)對(duì)象1、2和4的信息,這些統(tǒng)計(jì)信息放在對(duì)象所屬的頁(yè)面中。
這里把這些統(tǒng)計(jì)信息稱(chēng)為標(biāo)記信息,主要包括頁(yè)面中存活對(duì)象的個(gè)數(shù),對(duì)象經(jīng)過(guò)內(nèi)存對(duì)齊之后占用的內(nèi)存大小和對(duì)象的標(biāo)記位圖信息。

這里沒(méi)有演示應(yīng)用程序并發(fā)執(zhí)行的情況,如果應(yīng)用程序新分配對(duì)象,一定是從一個(gè)新的頁(yè)面中分配,因?yàn)閷?duì)象的緩沖頁(yè)面在初始標(biāo)記中被清空。

如果是新頁(yè)面,則不會(huì)在本輪垃圾回收中回收,所以在圖中沒(méi)有體現(xiàn)。另外,如果應(yīng)用程序線程訪問(wèn)待標(biāo)記的對(duì)象,則通過(guò)讀屏障完成標(biāo)記,其處理的方法和并發(fā)標(biāo)記中的方法完全一樣,圖中也沒(méi)有體現(xiàn)。
在并發(fā)標(biāo)記結(jié)束時(shí),所有頁(yè)面中的活躍對(duì)象都被標(biāo)記,同時(shí)標(biāo)記條帶一定為空。進(jìn)入再標(biāo)記階段
再標(biāo)記主要是為了處理因應(yīng)用程序線程標(biāo)記對(duì)象,導(dǎo)致仍然有待標(biāo)記的對(duì)象。實(shí)際上在并發(fā)標(biāo)記結(jié)束前會(huì)嘗試多次主動(dòng)刷新(和被動(dòng)刷新,文章中沒(méi)有介紹)以避免這種狀況,但是這種狀況不能完全避免,如果需要完全避免,必須在STW中進(jìn)行。另外,再標(biāo)記還會(huì)處理部分非強(qiáng)根的標(biāo)記。為了簡(jiǎn)化,本例中假定所有的對(duì)象在并發(fā)標(biāo)記中都標(biāo)記完成,也沒(méi)有非強(qiáng)根引用。

非強(qiáng)引用處理之后,將重置轉(zhuǎn)移集。主要是針對(duì)前一次垃圾回收過(guò)程中產(chǎn)生的對(duì)象地址信息表重置。在本例中因?yàn)槭堑谝淮卫厥账圆粫?huì)涉及重置。

重置轉(zhuǎn)移集之后,將回收無(wú)效的頁(yè)面。如果在頁(yè)面分配時(shí)內(nèi)存不足,將回收預(yù)分配或者緩存頁(yè)面中的頁(yè)面對(duì)應(yīng)的物理地址。本例中假定內(nèi)存充足,不會(huì)執(zhí)行本步。

在并發(fā)標(biāo)記之后,一共有4個(gè)頁(yè)面被標(biāo)記。在這一步中,會(huì)根據(jù)這4個(gè)頁(yè)面的統(tǒng)計(jì)信息選擇——哪些頁(yè)面可以回收。ZGC只會(huì)選擇頁(yè)面中垃圾空間超過(guò)頁(yè)面空間的25%的頁(yè)面,然后把所有選擇到的頁(yè)面根據(jù)頁(yè)面中垃圾空間大小排序,根據(jù)排序結(jié)果計(jì)算是否存在一些頁(yè)面在轉(zhuǎn)移后導(dǎo)致新的頁(yè)面無(wú)剩余空間,如果存在,則把這些頁(yè)面也丟棄,不進(jìn)行轉(zhuǎn)移。

在本例中,假定有一個(gè)小頁(yè)面和中頁(yè)面將被轉(zhuǎn)移,所以它們將進(jìn)入轉(zhuǎn)移集


image.png

選擇轉(zhuǎn)移集結(jié)束后,將對(duì)轉(zhuǎn)移集中的頁(yè)面初始化,初始化最終的動(dòng)作是初始化對(duì)象地址轉(zhuǎn)移信息表。轉(zhuǎn)移信息表將存儲(chǔ)轉(zhuǎn)移完成后對(duì)象轉(zhuǎn)移前后的地址。初始化轉(zhuǎn)移集結(jié)束后,整個(gè)內(nèi)存的對(duì)象關(guān)系如圖


image.png

接著將進(jìn)入初始轉(zhuǎn)移階段。進(jìn)入轉(zhuǎn)移階段后,地址視圖再次從M0切換到Remapped。初始轉(zhuǎn)移從根集合出發(fā),遍歷對(duì)象,對(duì)象進(jìn)行轉(zhuǎn)移或者調(diào)整對(duì)象的視圖。初始轉(zhuǎn)移結(jié)束后,整個(gè)內(nèi)存的對(duì)象關(guān)系如圖


image.png

可以看到對(duì)象4所在的頁(yè)面在轉(zhuǎn)移集中,所以它會(huì)被轉(zhuǎn)移到新的頁(yè)面中。對(duì)象4轉(zhuǎn)移之后,會(huì)在所在的頁(yè)面中記錄對(duì)象轉(zhuǎn)移信息。對(duì)象1和對(duì)象2不在轉(zhuǎn)移集中,所以它們不會(huì)被轉(zhuǎn)移,但是它們的地址視圖會(huì)從M0調(diào)整為Remapped。

初始轉(zhuǎn)移只會(huì)針對(duì)根集合進(jìn)行,結(jié)束后將進(jìn)入并發(fā)轉(zhuǎn)移,并發(fā)轉(zhuǎn)移是根據(jù)轉(zhuǎn)移集的頁(yè)面進(jìn)行遍歷,即只會(huì)遍歷轉(zhuǎn)移集選擇的頁(yè)面。在遍歷時(shí),兩個(gè)并發(fā)工作線程會(huì)根據(jù)標(biāo)記信息逐個(gè)轉(zhuǎn)移對(duì)象。轉(zhuǎn)移過(guò)程涉及內(nèi)存的復(fù)制,比較耗時(shí),所以在轉(zhuǎn)移時(shí)會(huì)把一個(gè)頁(yè)面分成64個(gè)段并發(fā)的轉(zhuǎn)移。因?yàn)閷?duì)象4已經(jīng)完成轉(zhuǎn)移,所以并發(fā)轉(zhuǎn)移會(huì)繼續(xù)轉(zhuǎn)移剩下的對(duì)象5和對(duì)象8,轉(zhuǎn)移后分別稱(chēng)為對(duì)象5’和對(duì)象8’。轉(zhuǎn)移后也會(huì)更新對(duì)象轉(zhuǎn)移信息表。對(duì)象5屬于小頁(yè)面對(duì)象,轉(zhuǎn)移后也會(huì)在小頁(yè)面中,對(duì)象8屬于中頁(yè)面對(duì)象,所以轉(zhuǎn)移中會(huì)新分配一個(gè)中頁(yè)面來(lái)存儲(chǔ)對(duì)象8。


image.png

需要注意的是,雖然對(duì)象4’和對(duì)象5’都在一個(gè)新的頁(yè)面中,但是對(duì)象4’在并發(fā)轉(zhuǎn)移完成后,指向的還是對(duì)象5的地址而不會(huì)調(diào)整到對(duì)象5’。

并發(fā)轉(zhuǎn)移之后,轉(zhuǎn)移集中的頁(yè)面會(huì)被立即加入頁(yè)面緩存供新的頁(yè)面分配使用。
假設(shè)轉(zhuǎn)移之后,應(yīng)用程序線程在執(zhí)行過(guò)程中從對(duì)象4’訪問(wèn)了對(duì)象5’,從對(duì)象8’訪問(wèn)了對(duì)象9,此時(shí)會(huì)利用讀屏障對(duì)對(duì)象進(jìn)行重定位,此時(shí)整個(gè)內(nèi)存的對(duì)象關(guān)系如圖


image.png

對(duì)象4’將根據(jù)頁(yè)面中對(duì)象地址轉(zhuǎn)移信息表得到對(duì)象5的地址為對(duì)象5’,對(duì)象5’的地址視圖是Remapped,所以直接調(diào)整引用關(guān)系。對(duì)象8’訪問(wèn)對(duì)象9,對(duì)象9并未被轉(zhuǎn)移,它的地址視圖仍然為M0,所以此時(shí)會(huì)先調(diào)整對(duì)象9的地址視圖從M0到Remapped,然后調(diào)整引用關(guān)系(指向另一個(gè)視圖為remapped 的9)。

經(jīng)過(guò)一段時(shí)間的運(yùn)行,因?yàn)槟撤N原因再次觸發(fā)垃圾回收。垃圾回收觸發(fā),進(jìn)入初始標(biāo)記后,地址視圖切換為M1,然后從根集合出發(fā),開(kāi)始遍歷直接引用的對(duì)象。初始標(biāo)記結(jié)束后,整個(gè)內(nèi)存的對(duì)象關(guān)系如圖


image.png

在新一輪的并發(fā)標(biāo)記中,從標(biāo)記條帶開(kāi)始標(biāo)記。在標(biāo)記時(shí)會(huì)對(duì)頁(yè)面的標(biāo)記信息進(jìn)行復(fù)位。這里還要注意,在標(biāo)記前,對(duì)象10、對(duì)象11和其他對(duì)象稍有區(qū)別,其他對(duì)象的地址視圖為Remapped,對(duì)象10和對(duì)象11的地址視圖為M0,說(shuō)明它們?cè)谏弦惠喞厥盏臉?biāo)記階段被識(shí)別為活躍對(duì)象,但是它們所在的頁(yè)面沒(méi)有在轉(zhuǎn)移階段被轉(zhuǎn)移或者被訪問(wèn)。在標(biāo)記結(jié)束后,所有活躍對(duì)象的地址視圖都會(huì)調(diào)整到M1


image.png

接下來(lái)的步驟和我們介紹過(guò)的垃圾回收步驟完全相同,也不再介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一個(gè)詭異的MySQL查詢超時(shí),居然揭開(kāi)了隱藏了兩年的BUG 這一周線上碰到一個(gè)詭異的BUG。 線上有個(gè)定時(shí)任務(wù),這...
    CoderW在簡(jiǎn)書(shū)閱讀 590評(píng)論 0 1
  • JDK 11中低延遲垃圾回收器,設(shè)計(jì)目標(biāo): 停頓時(shí)間 < 10ms,不隨堆、活躍對(duì)象的大小而增加 支持8M...
    hedgehog1112閱讀 1,497評(píng)論 0 1
  • 文章來(lái)源于公眾號(hào)美團(tuán)技術(shù)團(tuán)隊(duì) ,作者王東 王偉 作者簡(jiǎn)介 王東,美團(tuán)信息安全資深工程師。 王偉,美團(tuán)信息安全技術(shù)專(zhuān)...
    碼農(nóng)小光閱讀 1,001評(píng)論 0 9
  • 一、來(lái)源 ZGC收集器是由Oracle公司研發(fā)的。2018年創(chuàng)建了JEP 333將ZGC提交給OpenJDK,推動(dòng)...
    希特文閱讀 1,154評(píng)論 1 1
  • 久違的晴天,家長(zhǎng)會(huì)。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí),離放學(xué)已經(jīng)沒(méi)多少時(shí)間了。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,865評(píng)論 16 22

友情鏈接更多精彩內(nèi)容