The Z Garbage Collector (ZGC) 【2】

Java 11 諸多新特性中,最重要的可能就是引入一個新的GC回收器:ZGC(The Z Garbage Collector)。這個GC,是Oracle爲了低延遲(暫停時間),大內存的場景而開發的一個新的垃圾回收器。java

爲啥須要這個新回收器

在Java10 這幾年通過幾個版本的改進,調優已經接近極限。從時間上來看,Hotspot最近一次發佈新的GC,是在2006年發佈的G1。當時,AWS最大的實例只有1核CPU, 1.7G內存。而現在,AWS上已經能夠買到128核,3904G內存的服務器實例了。 因此,ZGC的設計目標就是爲了適用於這些場景:大內存低延遲(10ms之內)而且對應用的性能影響低(對應用吞吐量的影響在15%之內)算法

之後也能夠在這個基礎之上,提供更多功能特性,如:多層heap結構(例如使用內存+固態磁盤組成一個混合堆區)服務器

幾個概念

在繼續介紹以前,有必要說明幾個GC中常見的基礎概念。多線程

  • 並行(Parallel)
    • 有多個GC線程
    • 應用線程執行,不肯定
  • 串行(Serial)
    • 只有單個GC線程
    • 應用線程執行,不肯定
  • 暫停時間(Stop The World)
    • 中止應用線程的執行
    • 期間執行GC
  • 併發(Concurrent)
    • GC線程和應用線程同時都在運行
    • 也便是:內存分配器和垃圾回收器同時處於工做狀態
  • 增量(Incremental)
    • 也便是因爲某個條件觸發的,額外(更高級別)的一些GC過程

取捨

GC對於性能,須要作出一些取捨。 例如:並行GC能夠利用多線程回收內存,但這也會帶來更多CPU線程的開銷(諸如上下文切換); 相似的,併發GC不會暫停應用的運行,可是這也會顯著的須要更多的內存,同時因爲GC線程和應用線程同時運行,這也增長了調度上的複雜度。併發

ZGC

有了上述幾個基本概念,接下來,再來看看ZGC是如何工做的。 爲了達到ZGC的設計目標,其使用了Hotspot VM 的兩個新特性:app

  • 指針着色
  • 讀屏障

指針着色

這個技術就是,在指針上存放一些信息。(Java中的引用)ide

在64位平臺上(ZGC只支持64位),一個指針的尋址範圍會很是大,這樣,就能夠利用指針的某些位,來表示一些狀態位。 ZGC規定最大使用的堆,大小爲4TB,這樣的話,對於64位指針只須要使用42位就能夠尋址4TB的空間,那麼多餘出來的22位就能夠用於存放一些其餘狀態信息。 (指針長度一般是一個字(WORLD)的長度,而在64位平臺,一個字長64位)性能

目前,使用了22位中的4位長度,分別用來表示:是否已經finalize,重映射(remap),mark0,mark1。測試

指針着色,也會帶來一個問題。當解引用的時候,須要對其中的信息進行遮掩。(相似ip地址中的掩碼) 這個工做,在某些平臺是原生就支持的。如:SPARC; 而在x86平臺上則不支持。 爲了解決跨平臺的兼容性,ZGC團隊使用了multi-mapping技術來解決兼容問題。spa

multi-mapping

要想理解multi-mapping,須要先來了解虛擬內存和物理內存的區別。

  • 物理內存
    • 真實的ram存儲介質
  • 虛擬內存
    • 抽象:在應用看來本身可以使用的內存

操做系統經過如下幾個技術來管理和維護虛擬內存和物理內存的映射關係:

  • 頁表(page table)
  • 進程內存管理單元(Memory Management Unit (MMU) )
  • 頁表緩衝(Translation Lookaside Buffer (TLB) )
    • 對應用中的內存地址進行轉換

multi-mapping就是把不一樣區域的虛擬內存映射到同一塊物理內存的技術。 經過remap, mark0, mark1 三個狀態字來控制映射過程。

圖示以下:

//
// Page Allocation Tiers
// ---------------------
//
//  Page Type     Page Size     Object Size Limit     Object Alignment
//  ------------------------------------------------------------------
//  Small         2M            <= 265K               <MinObjAlignmentInBytes>
//  Medium        32M           <= 4M                 4K
//  Large         X*M           > 4M                  2M
//  ------------------------------------------------------------------
//
//
// Address Space & Pointer Layout
// ------------------------------
//
//  +--------------------------------+ 0x00007FFFFFFFFFFF (127TB)
//  .                                .
//  .                                .
//  .                                .
//  +--------------------------------+ 0x0000140000000000 (20TB)
//  |         Remapped View          |
//  +--------------------------------+ 0x0000100000000000 (16TB)
//  |     (Reserved, but unused)     |
//  +--------------------------------+ 0x00000c0000000000 (12TB)
//  |         Marked1 View           |
//  +--------------------------------+ 0x0000080000000000 (8TB)
//  |         Marked0 View           |
//  +--------------------------------+ 0x0000040000000000 (4TB)
//  .                                .
//  +--------------------------------+ 0x0000000000000000
//
//
//   6                 4 4 4  4 4                                             0
//   3                 7 6 5  2 1                                             0
//  +-------------------+-+----+-----------------------------------------------+
//  |00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
//  +-------------------+-+----+-----------------------------------------------+
//  |                   | |    |
//  |                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
//  |                   | |
//  |                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0      (Address view 4-8TB)
//  |                   |                                 0010 = Marked1      (Address view 8-12TB)
//  |                   |                                 0100 = Remapped     (Address view 16-20TB)
//  |                   |                                 1000 = Finalizable  (Address view N/A)
//  |                   |
//  |                   * 46-46 Unused (1-bit, always zero)
//  |
//  * 63-47 Fixed (17-bits, always zero)
//

讀屏障

在G1回收器中,使用寫屏障(write-barriers); 而ZGC中大量使用讀屏障(Load barriers),相似於:

void printName( Person person ) {
    String name = person.name;  // 讀屏障,從堆中加載引用
    System.out.println(name);   // 不在使用讀屏障,直接使用,此時的name不在堆中加載(局部變量)
}
  • 讀屏障,主要在於對引用狀態的檢查,另外,還會在引用返回以前進行一些工做
    • ZGC就是利用這個動做,在引用返回以前去檢查着色指針的幾個狀態位
      • 若是檢查經過(狀態符合要求),則引用正常使用
      • 若是檢查不經過,那麼就會在應用返回以前,ZGC就會根據不一樣狀態來執行一些額外動做

標記

在ZGC進行GC的處理週期中,第一步就是標記。這一步,就是爲了從堆中找出全部可到達的對象並在這些對象上打上標記。(也就是,找出全部不是垃圾的對象)

而ZGC的標記階段,又分爲三個步驟:

1. STW(Stop The World),將ROOT對象標記爲存活

image

  • ROOT就相似於局部變量,從這些對象上,能夠引用堆中的對象
  • 若是某個對象在對象引用圖中不可達,也就是從任何ROOT都沒法引用到,那麼就認爲這個對象是一個垃圾
  • 而在引用圖中可到達的對象集合,則被認爲是存活對象集
  • 這個階段只是找出ROOT對象,並標記這些ROOT對象存活
    • 因此很快,STW時間很短

2. 併發掃描對象引用圖,標記全部可達對象

  • 在這個步驟中,讀屏障檢查全部引用
    • 經過掩碼獲取引用中的狀態信息,判斷這個引用是否已經標記過
      • 若是還未標記,則放入標記隊列中

3. STW,處理一些狀態以及某些邊界值處理

  • 這一階段完成後,整個標記階段完成

重定位

標記階段完成,接下來就是重定位階段。重定位,就是把GC存活對象從新安放,從而釋放堆區空間。

那麼,爲何要移動這些對象,而不是簡單的直接填充而後直接釋放呢?

  • 有些GC算法就是如此,直接釋放
  • 但這樣會致使,接下來再次申請內存的時候,就要花費更大的代價
    • 內存碎片

這個階段,ZGC會進行幾個步驟:

  1. 找出須要重定位的內存頁
    • ZGC將整個堆分爲幾個頁來管理,在重定位這個階段開始時:
      • 併發的找出幾個須要重定位的內存頁
  2. 當重定位的頁已經找好了,那麼就會觸發一次STW暫停
    • 從ROOT對象開始,對全部對象進行重定位
    • 對這些對象的引用,從新映射到新的地址
    • 相似上一個階段,這一次STW也只是對ROOT對象進行
      • 這個對象集很小,因此STW時間很短
      • 並且這個集合大小和堆大小無關,因此基原本說這一次的STW是一個可預估的時長
  3. 當ROOT對象移動完成以後,接下來會併發執行重定位
    • 這個步驟,會把內存頁中全部對象進行重定位並進行移動
    • 這個過程不影響應用使用對象引用
      • 利用讀屏障,具體以下圖: image
      • 經過這個機制保障了應用中全部對象引用的可見性,而且也保障了重定位操做能夠於應用併發執行。
  4. 因爲對象引用發生遷移,重定位到了新的位置,而爲了保障其餘對象還能經過老的引用可以訪問對象,就須要將引用的映射關係保存起來(REMAP)
    • 而這個REMAP,則會在下一次標記階段清理,從而不會讓這個REMAP膨脹過大

小結

ZGC在實際中的性能還不能徹底說就必定好,不過從這個版原本看,幾回STW都是隻對ROOT對象集合,而且這個集合大小和堆大小無關。在標記階段最後一個STW,是某些條件下才出發的,而且是隻須要處理增量數據,而不是全量。並且,併發執行的標記階段也能夠在超過預期中的時間,也能夠返回,下一次再處理。

性能

有關ZGC的性能,Stefan Karlsson 和 Per Liden 給出了一些數據,能夠在youtube找到。經過SPECjbb 2015的數據,能夠和Parallel GC大概有個比較,平均暫停時間基本在1~4ms。而G1 和 Parallel 則大體須要200ms級別。

不過,垃圾回收是一個十分複雜的工做。基準測試的數據不能徹底表明實際狀況。因此,ZGC的性能還須要進一步在實際線上系統中驗證。

將來的一些可能

指針着色和讀屏障爲之後提供了一些改進的可能:

多層帶壓縮的堆

隨着閃存以及一些非易失存儲(non-volatile memory)的發展,能夠在JVM中構建一個多層的堆結構。 指針着色中目前只是用了4位,還有不少的空間能夠存放一些元數據。這樣,之後就有可能在讀屏障時去其餘存儲中查找對象。

因爲讀取引用是能夠經過讀屏障來操做,那麼理論上就能夠在度屏障時,對數據進行壓縮或者解壓。

目前的狀態

目前ZGC還處於一個實驗性的階段,還須要一段時間的驗證。G1從發佈到實際可用,也是通過了三年的時間。

總結

服務器如今動輒上百G,甚至TB級別的內存,同時,Java對於堆的使用效率愈來愈重要。ZGC就是爲了超大堆、低延遲的場景而設計的。其經過指針着色,以及讀屏障等技術,使得ZGC爲Hotspot的GC提供了一些可能。

原文: Java's new Z Garbage Collector (ZGC) is very exciting

相關文章
相關標籤/搜索