深入分析 Java ZGC

傳統的垃圾回收

我們在開發 Java 程序時,並不需要顯示釋放內存,Java 的垃圾回收器會自動幫我們回收。GC 會自動監測對象引用,並釋放不可達的對象。GC 需要監測堆內存中對象的狀態,如果一個對象不可達,GC 就可以考慮回收這個對象。

CMS 與 G1 停頓時間瓶頸

在介紹 ZGC 之前,首先回顧一下 CMS 和 G1 的 GC 過程以及停頓時間的瓶頸。CMS 新生代的 Young GC、G1 和 ZGC 都基於標記-複製算法,但算法具體實現的不同就導致了巨大的性能差異。

標記-複製算法應用在 CMS 新生代(ParNew 是 CMS 默認的新生代垃圾回收器)和 G1 垃圾回收器中。標記-複製算法可以分爲三個階段:

  • 標記階段,即從 GC Roots 集合開始,標記活躍對象;
  • 轉移階段,即把活躍對象複製到新的內存地址上;
  • 重定位階段,因爲轉移導致對象的地址發生了變化,在重定位階段,所有指向對象舊地址的指針都要調整到對象新的地址上。

CMS 在 JDK11 已經被 G1 所取代,G1 GC 的詳細算法可以參考文章:JVM G1GC 的算法與實現

ZGC 概覽

The Z Garbage Collector, also known as ZGC, is a scalable low latency garbage collector designed to meet the following goals:

  • Sub-millisecond max pause times
  • Pause times do not increase with the heap, live-set or root-set size
  • Handle heaps ranging from a 8MB to 16TB in size

總結下來就是:

  • 停頓時間不超過 10ms;
  • 停頓時間不會隨着堆的大小,或者活躍對象的大小而增加;
  • 支持 8MB~4TB 級別的堆,未來支持 16TB。

ZGC was initially introduced as an experimental feature in JDK 11, and was declared Production Ready in JDK 15.

ZGC 的主要特點:

  • Concurrent
  • Region-based
  • Compacting
  • NUMA-aware
  • Using colored pointers
  • Using load barriers

At its core, ZGC is a concurrent garbage collector, meaning all heavy lifting work is done while Java threads continue to execute. This greatly limits the impact garbage collection will have on your application's response time.

This OpenJDK project is sponsored by the HotSpot Group.

ZGC 有一個“marking”的階段,可以找到可達對象。GC 可以使用多種方法來存儲對象的狀態信息:比如創建一個 Map,key 是內存地址,value 是該地址上對象的狀態信息。這種方法雖然簡單,但是需要使用額外的內存來存儲這些狀態;同時維護這樣的 Map 也是一個挑戰。

ZGC 使用了一種完全不同的叫 着色指針(reference coloring) 方法:使用對象引用中的特定比特位來存儲對象的狀態。但是這種方法也有一個挑戰,使用引用位來存儲對象的元信息意味着多個引用可以指向同一個對象,因爲對象位並不保存有關對象位置的任何信息。我們可以使用多重映射來解決此問題。

我們還希望解決內存碎片的問題。ZGC 使用 relocation 來解決這個問題。但是對於一個很大的堆來說,relocation 過程會非常慢。因爲 ZGC 並不希望有很長的延時,ZGC 會將大多數的 relocation 過程與應用程序並行執行。但是這又引入了另一個問題。

比方說我們有了一個對象的引用,ZGC relocation 了這個對象,緊接着發生了線程的上下文切換,用戶線程正在試圖獲取這個對象的舊內存地址。ZGC 使用 讀屏障(load barriers) 來解決這個問題。load barrier 是線程從堆中獲取一個對象引用時加入的一小段代碼——比如我們需要訪問一個對象的非原始類型的字段。

在 ZGC 中,load barrier 會檢查引用元信息中的特定位,根據這些位的信息,ZGC 可能會在我們得到引用之前做一些處理,可能產生一個完全不同的引用,我們稱這個過程爲“重映射 remapping”。

深入 ZGC 原理

標記 Marking

ZGC 將標記分爲 3 個階段:

  • stop-the-world 階段。在這個階段,我們尋找並標記根引用(root references)。根引用是堆中可達對象的起點,可以是局部變量或靜態字段。這個階段通常時間非常短,因爲根引用的數量一般都非常小;
  • concurrent 階段。在這個階段,我們從根引用開始遍歷對象圖,並標記每個到達的對象;
  • stop-the-world 階段。處理一些如弱引用的邊緣情況。

此時我們就知道哪些對象是可達的。ZGC 使用 marked0 和 marked1 元數據位進行標記。

着色指針 Reference Coloring

一個引用就代表虛擬內存中一個字節的位置。我們並不需要使用引用的所有位來標識位置。在 32 位系統中,我們只能尋址 4GB 內存。由於現代計算機基本都有比這更多的內存,我們顯然不能佔用着 32 位中的任意一位。因此 ZGC 需要使用 64 位引用,這也就意味着 ZGC 僅適用於 64 位平臺。

ZGC 引用使用 42 位來表示地址,引用可以尋址 4TB 的內存空間。最重要的是,我們有 4 位來存儲引用的狀態:

  • finalizable 位:該對象只能通過終結器(finalizer)訪問
  • remap 位:引用是最新的,並指向對象的當前位置
  • marked0 和 marked11 位:標記可達對象

我們稱這些位爲元數據位,ZGC 中這些位有且僅有一個位是 1。

Relocation

在 ZGC 中,Relocation 包括以下幾個階段:

  • 併發階段。查找需要重新定位的塊,將它們加入 Relocation 候選集合。
  • stop-the-world 階段。重定位重定位集中的所有根引用並更新它們的引用。
  • 併發節點。將重定位集中的所有剩餘對象重定位,並將舊地址和新地址之間的映射存儲在轉發表中。
  • 剩餘引用的重寫發生在下一個標記階段。我們不需要兩次遍歷對象樹。

重映射和讀屏障 Remapping and Load Barriers

讀屏障是 JVM 嚮應用代碼插入一小段代碼的技術。當應用線程從堆中讀取對象引用時,就會執行這段代碼。需要注意的是,僅“從堆中讀取對象引用”纔會觸發這段代碼。

讀屏障示例:

Object o = obj.FieldA   // 從堆中讀取引用,需要加入屏障
<Load barrier>
Object p = o  // 無需加入屏障,因爲不是從堆中讀取引用
o.dosomething() // 無需加入屏障,因爲不是從堆中讀取引用
int i =  obj.FieldB  //無需加入屏障,因爲不是對象引用

ZGC 中讀屏障的代碼作用:在對象標記和轉移過程中,用於確定對象的引用地址是否滿足條件,並作出相應動作。

ZGC 併發處理演示

接下來詳細介紹 ZGC 一次垃圾回收週期中地址視圖的切換過程:

  • 初始化:ZGC 初始化之後,整個內存空間的地址視圖被設置爲 Remapped。程序正常運行,在內存中分配對象,滿足一定條件後垃圾回收啓動,此時進入標記階段。
  • 併發標記階段:第一次進入標記階段時視圖爲 M0,如果對象被 GC 標記線程或者應用線程訪問過,那麼就將對象的地址視圖從 Remapped 調整爲 M0。所以,在標記階段結束之後,對象的地址要麼是 M0 視圖,要麼是 Remapped。如果對象的地址是 M0 視圖,那麼說明對象是活躍的;如果對象的地址是 Remapped 視圖,說明對象是不活躍的。
  • 併發轉移階段:標記結束後就進入轉移階段,此時地址視圖再次被設置爲 Remapped。如果對象被 GC 轉移線程或者應用線程訪問過,那麼就將對象的地址視圖從 M0 調整爲 Remapped。

其實,在標記階段存在兩個地址視圖 M0 和 M1,上面的過程顯示只用了一個地址視圖。之所以設計成兩個,是爲了區別前一次標記和當前標記。也即,第二次進入併發標記階段後,地址視圖調整爲 M1,而非 M0。

着色指針和讀屏障技術不僅應用在併發轉移階段,還應用在併發標記階段:將對象設置爲已標記,傳統的垃圾回收器需要進行一次內存訪問,並將對象存活信息放在對象頭中;而在 ZGC 中,只需要設置指針地址的第 42~45 位即可,並且因爲是寄存器訪問,所以速度比訪問內存更快。

支持平臺

ZGC 性能對比

吞吐量對比

停頓時間對比

嗯,對比還是很明顯的……

快速開始

通過下面的參數,能夠啓用 ZGC。

-XX:+UseZGC -Xmx<size> -Xlog:gc

如果想獲取更多詳細 log,可以使用下面的參數:

-XX:+UseZGC -Xmx<size> -Xlog:gc*

變更記錄

JDK 17

  • Dynamic Number of GC threads
  • Reduced mark stack memory usage
  • macOS/aarch64 support
  • GarbageCollectorMXBeans for both pauses and cycles
  • Fast JVM termination

JDK 16

  • Concurrent Thread Stack Scanning (JEP 376)
  • Support for in-place relocation
  • Performance improvements (allocation/initialization of forwarding tables, etc)

JDK 15

  • Production ready (JEP 377)
  • Improved NUMA awareness
  • Improved allocation concurrency
  • Support for Class Data Sharing (CDS)
  • Support for placing the heap on NVRAM
  • Support for compressed class pointers
  • Support for incremental uncommit
  • Fixed support for transparent huge pages
  • Additional JFR events

JDK 14

  • macOS support (JEP 364)
  • Windows support (JEP 365)
  • Support for tiny/small heaps (down to 8M)
  • Support for JFR leak profiler
  • Support for limited and discontiguous address space
  • Parallel pre-touch (when using -XX:+AlwaysPreTouch)
  • Performance improvements (clone intrinsic, etc)
  • Stability improvements

JDK 13

  • Increased max heap size from 4TB to 16TB
  • Support for uncommitting unused memory (JEP 351)
  • Support for -XX:SoftMaxHeapSIze
  • Support for the Linux/AArch64 platform
  • Reduced Time-To-Safepoint

JDK 12

  • Support for concurrent class unloading
  • Further pause time reductions

JDK 11

  • Initial version of ZGC
  • Does not support class unloading (using -XX:+ClassUnloading has no effect)

FAQ

ZGC 中的“Z”表示什麼?

ZGC 只是一個名字,Z 沒有什麼特殊含義。

發音是 "zed gee see" 還是 "zee gee see"?

沒有規定,兩者都可以。

GitHub 項目

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章