一、來(lái)源
- ZGC收集器是由Oracle公司研發(fā)的。2018年創(chuàng)建了JEP 333將ZGC提交給OpenJDK,推動(dòng)其進(jìn)入OpenJDK11的發(fā)布清單中。
二、ZGC的堆內(nèi)存布局
- 與Shenandoah和G1一樣,ZGC也采用基于Region的堆內(nèi)存布局。
- ZGC的Region具有動(dòng)態(tài)性。
- 動(dòng)態(tài)創(chuàng)建和銷毀
- 動(dòng)態(tài)的區(qū)域容量大小
分類如下:
- 小型Region(Small Region):容量固定為2MB,用于放置小于256KB的小對(duì)象。
- 中型Region(Medium Region):容量固定為32MB,用于放置大于等于256KB但小于4MB的對(duì)象。
- 大型Region(Large Region):容量不固定,可以動(dòng)態(tài)變化,但必須為2MB的整數(shù)倍,用于放置4MB或以上的大對(duì)象。每個(gè)大型Region中只會(huì)存放一個(gè)大對(duì)象,所以實(shí)際容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的實(shí)現(xiàn)中是不會(huì)被重分配的,因?yàn)閺?fù)制一個(gè)大對(duì)象的代價(jià)非常高昂。
三、并發(fā)整理算法的實(shí)現(xiàn)。
3.1 算法的由來(lái)
- G1收集器的篩選回收階段是stop the world的,但收集器線程間是并行的,之所以不和用戶線程并發(fā)執(zhí)行,是因?yàn)镚1只回收一部分Region,停頓時(shí)間是用戶可以控制的。所以并不著急去實(shí)現(xiàn),交給了ZGC去實(shí)現(xiàn)。
- 并且因?yàn)镚1為了不影響吞吐量才選擇stw的。停頓用戶線程可以最大幅度提高垃圾收集效率。
3.2 實(shí)現(xiàn)
3.2.1 讀屏障
指針的自愈能力
- 在ZGC中,當(dāng)讀取處于重分配集的對(duì)象時(shí),會(huì)被讀屏障攔截,通過(guò)轉(zhuǎn)發(fā)表記錄將訪問(wèn)轉(zhuǎn)發(fā)到新復(fù)制的對(duì)象上,并同時(shí)修正更新該引用的值,使其直接指向新對(duì)象。ZGC將這種行為叫做指針的“自愈能力”。
- 好處是:第一次訪問(wèn)舊對(duì)象訪問(wèn)會(huì)變慢,但也只會(huì)有一次變慢,當(dāng)“自愈”完成后,后續(xù)訪問(wèn)就不會(huì)變慢了。
- Shenandoah每次訪問(wèn)都慢,對(duì)比發(fā)現(xiàn),ZGC的執(zhí)行負(fù)載更低。
3.2.2 染色指針技術(shù)
3.2.2.1 HotSpot虛擬機(jī)的標(biāo)記實(shí)現(xiàn)方案有如下幾種:
- 把標(biāo)記直接記錄在對(duì)象頭上(如Serial收集器);
- 把標(biāo)記記錄在與對(duì)象相互獨(dú)立的數(shù)據(jù)結(jié)構(gòu)上(如G1、Shenandoah使用了一種相當(dāng)于堆內(nèi)存的1/64大小的,稱為BitMap的結(jié)構(gòu)來(lái)記錄標(biāo)記信息);
- 直接把標(biāo)記信息記在引用對(duì)象的指針上(如ZGC)
為什么會(huì)放在指針上呢?
- 追蹤式收集算法的標(biāo)記階段就是看有沒有引用,所以可以只和指針打交道而不管指針?biāo)玫膶?duì)象本身。
- 例如對(duì)象標(biāo)記過(guò)程就是打個(gè)三色標(biāo)記,這些標(biāo)記本質(zhì)上只和對(duì)象引用有關(guān),和對(duì)象本身無(wú)關(guān)。某個(gè)對(duì)象只有它的引用關(guān)系才能決定它的存活。
3.2.2.2 染色指針的解釋
- 染色指針是一種直接將少量額外的信息存儲(chǔ)在指針上的技術(shù)。目前在Linux下64位的操作系統(tǒng)中高18位是不能用來(lái)尋址的,但是剩余的46為卻可以支持64T的空間,到目前為止我們幾乎還用不到這么多內(nèi)存。于是ZGC將46位中的高4位取出,用來(lái)存儲(chǔ)4個(gè)標(biāo)志位,剩余的42位可以支持4TB(2的42次冪)的內(nèi)存,也直接導(dǎo)致ZGC可以管理的內(nèi)存不超過(guò)4TB,如圖所示:
- 限制:只能在64位系統(tǒng)上,因?yàn)閆GC設(shè)置就是用的42-46位,32位明顯不夠嘛。。并且不支持壓縮指針(這一塊可以參考Java對(duì)象模型中的OOP,meta中有一個(gè)Klass直接指向Klass,還一個(gè)壓縮指針)如下。
union _metadata {
之前都是oop,現(xiàn)在直接指向Klass了
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;

3.2.2.3 染色指針的設(shè)計(jì)
ZGC使用了內(nèi)存多重映射(Multi-Mapping)將多個(gè)不同的虛擬內(nèi)存地址映射到同一個(gè)物理內(nèi)存地址上,這是一種多對(duì)一映射。因?yàn)槿旧羔樦皇侵匦露x內(nèi)存中某些指針的其中幾位,OS又不支持,OS只會(huì)把整個(gè)指針當(dāng)做一個(gè)內(nèi)存地址來(lái)對(duì)待,只是它自己瞎想,為了解決這個(gè)問(wèn)題,使用了現(xiàn)代處理器的虛擬內(nèi)存映射技術(shù)
-
現(xiàn)代處理器一般使用請(qǐng)求分頁(yè)機(jī)制+虛擬內(nèi)存映射技術(shù)。
- 請(qǐng)求分頁(yè)機(jī)制把線性地址空間和物理地址空間分別劃分為大小相等的塊。這樣的塊稱為頁(yè)。通過(guò)在線性虛擬空間的頁(yè)和物理地址空間的頁(yè)建立映射表,分頁(yè)機(jī)制會(huì)進(jìn)行線性地址到物理地址的映射,完成線性地址到物理地址的轉(zhuǎn)換。
- Linus/x86-64平臺(tái)上的ZGC使用了多重映射將多個(gè)不同的虛擬內(nèi)存地址映射到同一個(gè)物理內(nèi)存地址上,多對(duì)一映射。意味著ZGC在虛擬內(nèi)存空間中看到的地址空間比實(shí)際的堆內(nèi)存容量更大。
-
把染色指針中的標(biāo)志位看做是地址的分段符,只要把這些不同的地址段映射到同一個(gè)物理地址空間就行了,經(jīng)過(guò)多重映射轉(zhuǎn)換后,就可以使用染色指針正常進(jìn)行尋址了。
- 標(biāo)志位就是上圖的Remapped,Marked1,Marked0。

3.2.2.4 染色指針的作用。
一旦某個(gè)Region的存活對(duì)象被移走之后,這個(gè)Region立即就能夠被釋放和重用掉,而不必等待整個(gè)堆中所有指向該Region的引用都被修正后才能清理,這使得理論上只要還有一個(gè)空閑Region,ZGC就能完成收集。而Shenandoah需要等到更新階段結(jié)束才能釋放回收集中的Region,如果Region里面對(duì)象都存活的時(shí)候,需要1:1的空間才能完成收集。染色指針可以大幅減少在垃圾收集過(guò)程中內(nèi)存屏障的使用數(shù)量,ZGC只使用了讀屏障。因?yàn)樾畔⒅苯泳S護(hù)在指針中。染色指針具備強(qiáng)大的擴(kuò)展性,
它可以作為一種可擴(kuò)展的存儲(chǔ)結(jié)構(gòu)用來(lái)記錄更多與對(duì)象標(biāo)記、重定位過(guò)程相關(guān)的數(shù)據(jù),以便日后進(jìn)一步提高性能。
四、ZGC的過(guò)程

并發(fā)標(biāo)記(Concurrent Mark):與G1、Shenandoah一樣,并發(fā)標(biāo)記是遍歷對(duì)象圖做可達(dá)性分析的階段,它的初始標(biāo)記和最終標(biāo)記也會(huì)出現(xiàn)短暫的停頓,整個(gè)標(biāo)記階段只會(huì)更新染色指針中的Marked 0、Marked 1標(biāo)志位。
-
并發(fā)預(yù)備重分配(Concurrent Prepare for Relocate):這個(gè)階段需要根據(jù)特定的查詢條件統(tǒng)計(jì)得出本次收集過(guò)程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會(huì)掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護(hù)成本。
- ZGC的重分配集只是決定里面的存活對(duì)象會(huì)被復(fù)制到其他的Region。不是為了效益回收
- JDK12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個(gè)階段完成的。
-
并發(fā)重分配(Concurrent Relocate):重分配是ZGC執(zhí)行過(guò)程中的核心階段,這個(gè)過(guò)程要把重分配集中的存活對(duì)象復(fù)制到新的Region上,并為重分配集中的每個(gè)Region維護(hù)一個(gè)轉(zhuǎn)發(fā)表(Forward Table),記錄從舊對(duì)象到新對(duì)象的轉(zhuǎn)向關(guān)系。
- ZGC收集器能僅從引用上就明確得知一個(gè)對(duì)象是否處于重分配集之中,如果用戶線程此時(shí)并發(fā)訪問(wèn)了位于重分配集中的對(duì)象,這次訪問(wèn)將會(huì)被預(yù)置的內(nèi)存屏障所截獲,然后立即根據(jù)Region上的轉(zhuǎn)發(fā)表記錄將訪問(wèn)轉(zhuǎn)發(fā)到新復(fù)制的對(duì)象上,并同時(shí)修正更新該引用的值,使其直接指向新對(duì)象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。
- ZGC的染色指針因?yàn)椤白杂保⊿elf-Healing)能力,所以只有第一次訪問(wèn)舊對(duì)象會(huì)變慢,而Shenandoah的Brooks轉(zhuǎn)發(fā)指針是每次都會(huì)變慢。 一旦重分配集中某個(gè)Region的存活對(duì)象都復(fù)制完畢后,這個(gè)Region就可以立即釋放用于新對(duì)象的分配,但是轉(zhuǎn)發(fā)表還得留著不能釋放掉,因?yàn)榭赡苓€有訪問(wèn)在使用這個(gè)轉(zhuǎn)發(fā)表。
并發(fā)重映射(Concurrent Remap):重映射所做的就是修正整個(gè)堆中指向重分配集中舊對(duì)象的所有引用,但是ZGC中對(duì)象引用存在“自愈”功能,所以這個(gè)重映射操作并不是很迫切。ZGC很巧妙地把并發(fā)重映射階段要做的工作,合并到了下一次垃圾收集循環(huán)中的并發(fā)標(biāo)記階段里去完成,反正它們都是要遍歷所有對(duì)象的,這樣合并就節(jié)省了一次遍歷對(duì)象圖的開銷。
五、ZGC的優(yōu)點(diǎn)
- 低停頓,高吞吐量,ZGC收集過(guò)程中額外耗費(fèi)的內(nèi)存小
- G1通過(guò)寫屏障維護(hù)記憶集,才能處理跨代指針,得以實(shí)現(xiàn)增量回收。記憶集占用大量?jī)?nèi)存,寫屏障對(duì)正常程序造成額外負(fù)擔(dān)。
- ZGC沒有寫屏障,卡表之類的。
- 在多核處理器的某種架構(gòu)下,ZGC優(yōu)先在線程當(dāng)前所處的處理器的本地內(nèi)存上分配對(duì)象,以保證內(nèi)存高效訪問(wèn)。
六、ZGC的缺點(diǎn)
- 承受的對(duì)象分配速率不會(huì)太高,因?yàn)楦?dòng)垃圾。
- ZGC的停頓時(shí)間是在10ms以下,但是ZGC的執(zhí)行時(shí)間還是遠(yuǎn)遠(yuǎn)大于這個(gè)時(shí)間的。假如ZGC全過(guò)程需要執(zhí)行10分鐘,在這個(gè)期間由于對(duì)象分配速率很高,將創(chuàng)建大量的新對(duì)象,這些對(duì)象很難進(jìn)入當(dāng)次GC,所以只能在下次GC的時(shí)候進(jìn)行回收,這些只能等到下次GC才能回收的對(duì)象就是浮動(dòng)垃圾。
- 造成回收到的內(nèi)存空間小于期間并發(fā)產(chǎn)生的浮動(dòng)垃圾所占的空間。
ZGC沒有分代概念,每次都需要進(jìn)行全堆掃描,導(dǎo)致一些“朝生夕死”的對(duì)象沒能及時(shí)的被回收。
6.1 解決辦法
- 增加堆容量大小,使得程序得到更多的喘息時(shí)間。治標(biāo)不治本的方案。
- 從根本上解決這個(gè)問(wèn)題,還是需要引入分代收集。讓新生對(duì)象在一個(gè)專門區(qū)域創(chuàng)建,然后專門針對(duì)這個(gè)區(qū)域進(jìn)行更頻繁的,更快的收集。
參考資料
《深入理解Java虛擬機(jī)第三版》