深入分析 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 下載鏈接,需要的小夥伴可以自取~
原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。
參考資料
- //www.baeldung.com/jvm-zgc-garbage-collector
- //wiki.openjdk.java.net/display/zgc/Main#Main-JDK17
- 新一代垃圾回收器 ZGC 的探索與實踐
- ZGC-FOSDEM-2018.pdf
- JVM G1GC 的演算法與實現