理解JVM垃圾收集器-ZGC

1、來源

  • ZGC收集器是由Oracle公司研發的。2018年建立了JEP 333將ZGC提交給OpenJDK,推進其進入OpenJDK11的發佈清單中。

2、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、併發整理算法的實現。

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收集器);
  • 把標記記錄在與對象相互獨立的數據結構上(如G一、Shenandoah使用了一種至關於堆內存的1/64大小的,稱爲BitMap的結構來記錄標記信息);
  • 直接把標記信息記在引用對象的指針上(如ZGC)

爲何會放在指針上呢?app

  • 追蹤式收集算法的標記階段就是看有沒有引用,因此能夠只和指針打交道而無論指針所引用的對象自己。
  • 例如對象標記過程就是打個三色標記,這些標記本質上只和對象引用有關,和對象自己無關。某個對象只有它的引用關係才能決定它的存活。

3.2.2.2 染色指針的解釋

  • 染色指針是一種直接將少許額外的信息存儲在指針上的技術。目前在Linux下64位的操做系統中高18位是不能用來尋址的,可是剩餘的46爲卻能夠支持64T的空間,到目前爲止咱們幾乎還用不到這麼多內存。因而ZGC將46位中的高4位取出,用來存儲4個標誌位,剩餘的42位能夠支持4TB(2的42次冪)的內存,也直接致使ZGC能夠管理的內存不超過4TB,如圖所示:oop

    • 限制:只能在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在虛擬內存空間中看到的地址空間比實際的堆內存容量更大。
  • 把染色指針中的標誌位看作是地址的分段符,只要把這些不一樣的地址段映射到同一個物理地址空間就好了,通過多重映射轉換後,就可使用染色指針正常進行尋址了。spa

    • 標誌位就是上圖的Remapped,Marked1,Marked0。

ZGC多重映射下的尋址
ZGC多重映射下的尋址.jpg操作系統

3.2.2.4 染色指針的做用。

  • 一旦某個Region的存活對象被移走以後,這個Region當即就可以被釋放和重用掉,而沒必要等待整個堆中全部指向該Region的引用都被修正後才能清理,這使得理論上只要還有一個空閒Region,ZGC就能完成收集。而Shenandoah須要等到更新階段結束才能釋放回收集中的Region,若是Region裏面對象都存活的時候,須要1:1的空間才能完成收集。
  • 染色指針能夠大幅減小在垃圾收集過程當中內存屏障的使用數量,ZGC只使用了讀屏障。由於信息直接維護在指針中。
  • 染色指針具有強大的擴展性,它能夠做爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便往後進一步提升性能。

4、ZGC的過程

ZGC運做過程:
ZGC運做.jpg

  • 併發標記(Concurrent Mark):與G一、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很巧妙地把併發重映射階段要作的工做,合併到了下一次垃圾收集循環中的併發標記階段裏去完成,反正它們都是要遍歷全部對象的,這樣合併就節省了一次遍歷對象圖的開銷。

5、ZGC的優勢

  • 低停頓,高吞吐量,ZGC收集過程當中額外耗費的內存小
  • G1經過寫屏障維護記憶集,才能處理跨代指針,得以實現增量回收。記憶集佔用大量內存,寫屏障對正常程序形成額外負擔。
  • ZGC沒有寫屏障,卡表之類的。
  • 在多核處理器的某種架構下,ZGC優先在線程當前所處的處理器的本地內存上分配對象,以保證內存高效訪問。

6、ZGC的缺點

  1. 承受的對象分配速率不會過高,由於浮動垃圾。

    • ZGC的停頓時間是在10ms如下,可是ZGC的執行時間仍是遠遠大於這個時間的。假如ZGC全過程須要執行10分鐘,在這個期間因爲對象分配速率很高,將建立大量的新對象,這些對象很難進入當次GC,因此只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
    • 形成回收到的內存空間小於期間併發產生的浮動垃圾所佔的空間。
ZGC沒有分代概念,每次都須要進行全堆掃描,致使一些「朝生夕死」的對象沒能及時的被回收。

6.1 解決辦法

  1. 增長堆容量大小,使得程序獲得更多的喘息時間。治標不治本的方案。
  2. 從根本上解決這個問題,仍是須要引入分代收集。讓新生對象在一個專門區域建立,而後專門針對這個區域進行更頻繁的,更快的收集。

參考資料

《深刻理解Java虛擬機第三版》

相關文章
相關標籤/搜索