【JVM系列】低延遲迴收器 ZGC

槑!發表於2020-10-26
ZGC收集器

ZGC主要特徵

  • 基於Region記憶體佈局的,

  • (暫時)不設分代的,

  • 使用了讀屏障、染色指標和記憶體多重對映等技術來實現可併發的標記-整理演算法的,

  • 以低延遲為首要目標的一款垃圾收集器。

記憶體佈局(ZGC的Region有大、中、小三類容量)

  • 小型Region(Small Region):容量固定為2MB,用於放置小於256KB的小物件。

  • 中型Region(Medium Region):容量固定為32MB,用於放置大於等於256KB但小於4MB的物件。

  • 大型Region(Large Region):容量不固定,可以動態變化,但必須為2MB的整數倍,用於放置4MB或以上的大物件。

    • 每個大型Region中只會存放一個大物件,這也預示著雖然名字叫作“大型Region”,但它的實際容量完全有可能小於中型Region,最小容量可低至4MB。
    • 大型Region在ZGC的實現中是不會被重分配(重分配是ZGC的一種處理動作,用於複製物件的收集器階段)的,因為複製一個大物件的代價非常高昂。

四個大階段

在這裡插入圖片描述

  • 併發標記(Concurrent Mark):與G1、Shenandoah一樣,併發標記是遍歷物件圖做可達性分析的階段,前後也要經過類似於G1、Shenandoah的初始標記、最終標記(儘管ZGC中的名字不叫這些)的短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的。
    • 與G1、Shenandoah不同的是,ZGC的標記是在指標上而不是在物件上進行的,標記階段會更新染色指標中的Marked 0、Marked 1標誌位。
  • 併發預備重分配(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。
    • 重分配集與G1收集器的回收集(Collection Set)還是有區別的,ZGC劃分Region的目的並非為了像G1那樣做收益優先的增量回收。相反,ZGC每次回收都會掃描所有的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。
    • 因此,ZGC的重分配集只是決定了裡面的存活物件會被重新複製到其他的Region中,裡面的Region會被釋放,而並不能說回收行為就只是針對這個集合裡面的Region進行,因為標記過程是針對全堆的。
    • 此外,在JDK 12的ZGC中開始支援的類解除安裝以及弱引用的處理,也是在這個階段中完成的。
  • 併發重分配(Concurrent Relocate):重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活物件複製到新的Region上,併為重分配集中的每個Region維護一個轉發表(ForwardTable),記錄從舊物件到新物件的轉向關係。
    • 得益於染色指標的支援,ZGC收集器能僅從引用上就明確得知一個物件是否處於重分配集之中,如果使用者執行緒此時併發訪問了位於重分配集中的物件,這次訪問將會被預置的記憶體屏障所截獲,然後立即根據Region上的轉發表記錄將訪問轉發到新複製的物件上,並同時修正更新該引用的值,使其直接指向新物件,ZGC將這種行為稱為指標的“自愈”(Self-Healing)能力。
    • 這樣做的好處是隻有第一次訪問舊物件會陷入轉發,也就是隻慢一次,對比Shenandoah的Brooks轉發指標,那是每次物件訪問都必須付出的固定開銷,簡單地說就是每次都慢,因此ZGC對使用者程式的執行時負載要比Shenandoah來得更低一些。
    • 還有另外一個直接的好處是由於染色指標的存在,一旦重分配集中某個Region的存活物件都複製完畢後,這個Region就可以立即釋放用於新物件的分配(但是轉發表還得留著不能釋放掉),哪怕堆中還有很多指向這個物件的未更新指標也沒有關係,這些舊指標一旦被使用,它們都是可以自愈的。
  • 併發重對映(Concurrent Remap):重對映所做的就是修正整個堆中指向重分配集中舊物件的所有引用,這一點從目標角度看是與Shenandoah併發引用更新階段一樣的。
    • 但是ZGC的併發重對映並不是一個必須要“迫切”去完成的任務,因為前面說過,即使是舊引用,它也是可以自愈的,最多隻是第一次使用時多一次轉發和修正操作。重對映清理這些舊引用的主要目的是為了不變慢(還有清理結束後可以釋放轉發表這樣的附帶收益),所以說這並不是很“迫切”。
    • 因此,ZGC很巧妙地把併發重對映階段要做的工作,合併到了下一次垃圾收集迴圈中的併發標記階段裡去完成,反正它們都是要遍歷所有物件的,這樣合併就節省了一次遍歷物件圖的開銷。一旦所有指標都被修正之後,原來記錄新舊物件關係的轉發表就可以釋放掉了。

併發整理實現

  • 染色指標

    • 染色指標是一種直接將少量額外的資訊儲存在指標上的技術。

    • ZGC 實現

      • Linux下64位指標的高18位不能用來定址,但剩餘的46位指標所能支援的64TB記憶體在今天仍然能夠充分滿足大型伺服器的需要。鑑於此,ZGC的染色指標技術繼續盯上了這剩下的46位指標寬度,將其高4位提取出來儲存四個標誌資訊。
      • 通過這些標誌位,虛擬機器可以直接從指標中看到其引用物件的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。

      在這裡插入圖片描述

    • 三大優勢

      • 染色指標可以使得一旦某個Region的存活物件被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正後才能清理。因為通過染色指標就可以找到轉發表上對應的轉發方向,然後就可以找到複製後的物件。
      • 染色指標可以大幅減少在垃圾收集過程中記憶體屏障的使用數量。設定記憶體屏障,尤其是寫屏障的目的通常是為了記錄物件引用的變動情況,如果將這些資訊直接維護在指標中,顯然就可以省去一些專門的記錄操作。實際上,到目前為止ZGC都並未使用任何寫屏障,只使用了讀屏障(一部分是染色指標的功勞,一部分是ZGC現在還不支援分代收集,即沒有跨代引用的問題就不用寫屏障維護)。
      • 染色指標可以作為一種可擴充套件的儲存結構用來記錄更多與物件標記、重定位過程相關的資料,以便日後進一步提高效能。現在Linux下的64位指標還有前18位並未使用,它們雖然不能用來定址,卻可以通過其他手段用於資訊記錄。
    • 記憶體對映技術

      • 處理器處理時,指標哪部分存的是標誌位,哪部分才是真正的定址地址。
      • ZGC使用了多重對映(Multi-Mapping)將多個不同的虛擬記憶體地址對映到同一個實體記憶體地址上,這是一種多對一對映,意味著ZGC在虛擬記憶體中看到的地址空間要比實際的堆記憶體容量來得更大。
      • ZGC實現是把染色指標中的標誌位看作是地址的分段符,那隻要將這些不同的地址段都對映到同一個實體記憶體空間,經過多重對映轉換後,就可以使用染色指標正常進行定址了
  • 讀屏障

    • 對於第一次訪問舊物件會轉發,轉發表記錄將訪問轉發到新複製的物件上,並“自愈”,即同時修正更新該引用的值,使其直接指向新物件。

不分代的權衡

  • G1需要通過寫屏障來維護記憶集,才能處理跨代指標,得以實現Region的增量回收(實現分代 gc,只針對新生代的回收,因為肯定比老年代收益大)。記憶集要佔用大量的記憶體空間,寫屏障也對正常程式執行造成額外負擔,這些都是權衡選擇的代價。ZGC就完全沒有使用記憶集,它甚至連分代都沒有,連像CMS中那樣只記錄新生代和老年代間引用的卡表也不需要,因而完全沒有用到寫屏障,所以給使用者執行緒帶來的執行負擔也要小得多。
  • 可是,必定要有優有劣才會稱作權衡,ZGC的這種選擇也限制了它能承受的物件分配速率不會太高,因為是整堆回收,所以每次回收的時間間隔變長 ,並且每次併發回收過程中的時間變長,堆中剩餘可騰挪的空間就越來越小了。目前唯一的辦法就是儘可能地增加堆容量大小,獲得更多喘息的時間。但是若要從根本上提升ZGC能夠應對的物件分配速率,還是需要引入分代收集,讓新生物件都在一個專門的區域中建立,然後專門針對這個區域進行更頻繁、更快的收集。Azul的C4收集器實現了分代收集後,能夠應對的物件分配速率就比不分代的PGC收集器提升了十倍之多。

效能

  • 在ZGC的“弱項”吞吐量方面,以低延遲為首要目標的ZGC已經達到了以高吞吐量為目標Parallel Scavenge的99%,直接超越了G1。部分場景ZGC的表現甚至還反超了Parallel Scavenge收集器。
  • 而在ZGC的強項停頓時間測試上,它就毫不留情地與Parallel Scavenge、G1拉開了兩個數量級的差距。

相關文章