一、來源
- ZGC收集器是由Oracle公司研發的。2018年創建了JEP 333將ZGC提交給OpenJDK,推動其進入OpenJDK11的發布清單中。
二、ZGC的堆內存布局
- 與Shenandoah和G1一樣,ZGC也采用基于Region的堆內存布局。
- ZGC的Region具有動態性。
- 動態創建和銷毀
- 動態的區域容量大小
分類如下:
- 小型Region(Small Region):容量固定為2MB,用于放置小于256KB的小對象。
- 中型Region(Medium Region):容量固定為32MB,用于放置大于等于256KB但小于4MB的對象。
- 大型Region(Large Region):容量不固定,可以動態變化,但必須為2MB的整數倍,用于放置4MB或以上的大對象。每個大型Region中只會存放一個大對象,所以實際容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的實現中是不會被重分配的,因為復制一個大對象的代價非常高昂。
三、并發整理算法的實現。
3.1 算法的由來
- G1收集器的篩選回收階段是stop the world的,但收集器線程間是并行的,之所以不和用戶線程并發執行,是因為G1只回收一部分Region,停頓時間是用戶可以控制的。所以并不著急去實現,交給了ZGC去實現。
- 并且因為G1為了不影響吞吐量才選擇stw的。停頓用戶線程可以最大幅度提高垃圾收集效率。
3.2 實現
3.2.1 讀屏障
指針的自愈能力
- 在ZGC中,當讀取處于重分配集的對象時,會被讀屏障攔截,通過轉發表記錄將訪問轉發到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象。ZGC將這種行為叫做指針的“自愈能力”。
- 好處是:第一次訪問舊對象訪問會變慢,但也只會有一次變慢,當“自愈”完成后,后續訪問就不會變慢了。
- Shenandoah每次訪問都慢,對比發現,ZGC的執行負載更低。
3.2.2 染色指針技術
3.2.2.1 HotSpot虛擬機的標記實現方案有如下幾種:
- 把標記直接記錄在對象頭上(如Serial收集器);
- 把標記記錄在與對象相互獨立的數據結構上(如G1、Shenandoah使用了一種相當于堆內存的1/64大小的,稱為BitMap的結構來記錄標記信息);
- 直接把標記信息記在引用對象的指針上(如ZGC)
為什么會放在指針上呢?
- 追蹤式收集算法的標記階段就是看有沒有引用,所以可以只和指針打交道而不管指針所引用的對象本身。
- 例如對象標記過程就是打個三色標記,這些標記本質上只和對象引用有關,和對象本身無關。某個對象只有它的引用關系才能決定它的存活。
3.2.2.2 染色指針的解釋
- 染色指針是一種直接將少量額外的信息存儲在指針上的技術。目前在Linux下64位的操作系統中高18位是不能用來尋址的,但是剩余的46為卻可以支持64T的空間,到目前為止我們幾乎還用不到這么多內存。于是ZGC將46位中的高4位取出,用來存儲4個標志位,剩余的42位可以支持4TB(2的42次冪)的內存,也直接導致ZGC可以管理的內存不超過4TB,如圖所示:
- 限制:只能在64位系統上,因為ZGC設置就是用的42-46位,32位明顯不夠嘛。。并且不支持壓縮指針(這一塊可以參考Java對象模型中的OOP,meta中有一個Klass直接指向Klass,還一個壓縮指針)如下。
union _metadata {
之前都是oop,現在直接指向Klass了
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
3.2.2.3 染色指針的設計
ZGC使用了內存多重映射(Multi-Mapping)將多個不同的虛擬內存地址映射到同一個物理內存地址上,這是一種多對一映射。
因為染色指針只是重新定義內存中某些指針的其中幾位,OS又不支持,OS只會把整個指針當做一個內存地址來對待,只是它自己瞎想,為了解決這個問題,使用了現代處理器的虛擬內存映射技術
-
現代處理器一般使用請求分頁機制+虛擬內存映射技術。
- 請求分頁機制把線性地址空間和物理地址空間分別劃分為大小相等的塊。這樣的塊稱為頁。通過在線性虛擬空間的頁和物理地址空間的頁建立映射表,分頁機制會進行線性地址到物理地址的映射,完成線性地址到物理地址的轉換。
- Linus/x86-64平臺上的ZGC使用了多重映射將多個不同的虛擬內存地址映射到同一個物理內存地址上,多對一映射。意味著ZGC在虛擬內存空間中看到的地址空間比實際的堆內存容量更大。
-
把染色指針中的標志位看做是地址的分段符,只要把這些不同的地址段映射到同一個物理地址空間就行了,經過多重映射轉換后,就可以使用染色指針正常進行尋址了。
- 標志位就是上圖的Remapped,Marked1,Marked0。
3.2.2.4 染色指針的作用。
一旦某個Region的存活對象被移走之后,這個Region立即就能夠被釋放和重用掉
,而不必等待整個堆中所有指向該Region的引用都被修正后才能清理,這使得理論上只要還有一個空閑Region,ZGC就能完成收集。而Shenandoah需要等到更新階段結束才能釋放回收集中的Region,如果Region里面對象都存活的時候,需要1:1的空間才能完成收集。染色指針可以大幅減少在垃圾收集過程中內存屏障的使用數量
,ZGC只使用了讀屏障。因為信息直接維護在指針中。染色指針具備強大的擴展性,
它可以作為一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據
,以便日后進一步提高性能。
四、ZGC的過程
并發標記(Concurrent Mark):與G1、Shenandoah一樣,并發標記是遍歷對象圖做可達性分析的階段,它的初始標記和最終標記也會出現短暫的停頓,整個標記階段只會更新染色指針中的Marked 0、Marked 1標志位。
-
并發預備重分配(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護成本。
- ZGC的重分配集只是決定里面的存活對象會被復制到其他的Region。不是為了效益回收
- JDK12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個階段完成的。
-
并發重分配(Concurrent Relocate):重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活對象復制到新的Region上,并為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關系。
- ZGC收集器能僅從引用上就明確得知一個對象是否處于重分配集之中,如果用戶線程此時并發訪問了位于重分配集中的對象,這次訪問將會被預置的內存屏障所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。
- ZGC的染色指針因為“自愈”(Self-Healing)能力,所以只有第一次訪問舊對象會變慢,而Shenandoah的Brooks轉發指針是每次都會變慢。 一旦重分配集中某個Region的存活對象都復制完畢后,這個Region就可以立即釋放用于新對象的分配,但是轉發表還得留著不能釋放掉,因為可能還有訪問在使用這個轉發表。
并發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用,但是ZGC中對象引用存在“自愈”功能,所以這個重映射操作并不是很迫切。ZGC很巧妙地把并發重映射階段要做的工作,合并到了下一次垃圾收集循環中的并發標記階段里去完成,反正它們都是要遍歷所有對象的,這樣合并就節省了一次遍歷對象圖的開銷。
五、ZGC的優點
- 低停頓,高吞吐量,ZGC收集過程中額外耗費的內存小
- G1通過寫屏障維護記憶集,才能處理跨代指針,得以實現增量回收。記憶集占用大量內存,寫屏障對正常程序造成額外負擔。
- ZGC沒有寫屏障,卡表之類的。
- 在多核處理器的某種架構下,ZGC優先在線程當前所處的處理器的本地內存上分配對象,以保證內存高效訪問。
六、ZGC的缺點
- 承受的對象分配速率不會太高,因為浮動垃圾。
- ZGC的停頓時間是在10ms以下,但是ZGC的執行時間還是遠遠大于這個時間的。假如ZGC全過程需要執行10分鐘,在這個期間由于對象分配速率很高,將創建大量的新對象,這些對象很難進入當次GC,所以只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
- 造成回收到的內存空間小于期間并發產生的浮動垃圾所占的空間。
ZGC沒有分代概念,每次都需要進行全堆掃描,導致一些“朝生夕死”的對象沒能及時的被回收。
6.1 解決辦法
- 增加堆容量大小,使得程序得到更多的喘息時間。治標不治本的方案。
- 從根本上解決這個問題,還是需要引入分代收集。讓新生對象在一個專門區域創建,然后專門針對這個區域進行更頻繁的,更快的收集。
參考資料
《深入理解Java虛擬機第三版》