JVM 從入門到放棄之 ZGC 垃圾收集器
ZGC 概述
Z Garbage Collector,也稱為ZGC,在 jdk 11 中引入的一種可擴展的低延遲垃圾收集器,在 jdk 15 中發布穩定版。在旨在滿足以下目標:
- < 1ms 最大暫停時間(jdk < 16 是 10ms,jdk >=16 是 <1ms )。
- 暫停時間不會隨著堆、live-set 或 root-set 的大小而增加。
- 適用內存大小從 8MB 到16TB 的堆。
ZGC 具有以下特征:
- 并發
- 基于 region
- 壓縮
- NUMA 感知
- 使用彩色指針
- 使用負載屏障
ZGC 的核心是一個并發垃圾收集器,這意味著所有繁重的工作都在Java 線程繼續執行的同時完成。這極大地限制了垃圾收集對應用程序響應時間的影響。
ZGC 特征
ZGC 收集器是一款基于 Region 內存布局的,(暫時) 不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可并發的標記-整理算法的,以低延遲為首要目標的一款垃圾收集器。
內存布局
ZGC 沒有分代的概念
ZGC 的內存布局說起。與 Shenandoah 和 G1一樣,ZGC 也采用基于 Region 的堆內存布局,但與它們不同的是 , ZGC 的 Region 具 有 動 態 性 (動態創建和銷毀 , 以及動態的區域容量大小)。在 x64硬件平臺下 , ZGC 的 Region 可以具有大、中、小三類容量(如下圖所示):
- 小型 Region (Small Region ):容量固定為 2M, 存放小于 256K 的對象。
- 中型 Region (Medium Region):容量固定為 32M,放置大于等于256K但小于4M的對象。
- 大型 Region (Large Region): 容量不固定,可以動態變化,但必須為2MB 的整數倍,用于放置 4MB或以上的大對象。
NUMA-aware
NUMA 對應的有 NMA 、UMA 即 Uniform Memory Access Architecture, NUMA 就是 Non Uniform Memory Access Architecture. UMA 表示內存只有一塊,所有的 CUU 都要去訪問這些內存,那么會存在競爭問題(競爭內存總線訪問權),有競爭就要去加鎖,有鎖效率就會受到影響,而且 CPU 核心數越多,競爭就越激烈。NUMA 的話每個 CPU 對應有一個內存塊,且這塊內存在主板上離這個 CPU 是最近的,每個 CPU 優先訪問這塊內存,那效率就自然提高了。
服務器的 NUMA 架構在中大型系統上非常流行,也就是高性能的解決方案,尤其在系統延遲方面表現非常優秀,ZGC 是能自動感知 NUMA 架構并且充分利用 NUMA 架構的特征。
染色指針(Colored Pointer)
Colored Pointer, 即染色指針,如圖所示, ZGC 的核心設計之一。以前的垃圾收集器的 GC 信息都保存在對象頭中,而 ZGC 的 GC 信息保存在指針中(直接把標記信息記錄在對象的引用指針上)。
每個對象有一個64位指針,這64位被分為:
- 18位:預留給以后使用。
- 1位:Finalizable標識,此位與并發引用處理有關,它表示這個對象只能通過finalizer才能訪問(finalizer:object基類的一個空方法,如果被重寫則會在GC之前調用該方法,該方法會且只會被調用一次)。
- 1位:Remapped 標識,設置此位的值后,對象未指向relocation set中(relocation set表示需要GC的Region集合)。
- 1位:Marked1標識。
- 1位:Marked0標識,和上面的Marked1都是標記對象用于輔助GC。
- 42位:對象的地址(所以它可以支持2^42=4T內存):
為什么會有兩個 mark 標記?
每一個GC周期開始時,會交換使用的標記位,使上次GC周期中修正的已標記狀態失效,所有引用都變成未標記。GC周期1:使用mark0, 則周期結束所有引用mark標記都會成為 01。GC周期2:使用mark1, 與周期1相同,所有的mark標記都會成為 10。
ZGC不能做指針壓縮?
指針壓縮指的是壓縮為32位,尋址位數不能超過35,也就是JVM內存最大為32G(2^35=32GB),這里的尋址位數已經達到了42位。
顏色指針的三大優勢 ?
- 在一個Region中的所有存活對象都被移走后(復制走后),這個Region就可以被立即釋放掉,因為它還有轉發表記錄著原始地址和新地址,這樣的話,理論上,只要還有一個Region對象空閑,ZGC就能完成垃圾收集。
- 顏色指針有指針的“自愈”(Self-Healing)能力,這樣子就減少了寫屏障(例如三色標記中的增量更新或原始快照),只需要一個讀屏障就可以解決問題,減少了內存屏障的使用數量。
- 顏色指針有著極大的擴展性,因為還有18位未使用,這樣更有利于后續功能的擴展。
多重映射尋址
不同的虛擬機內存到物理內存的轉換關系可以在硬件層面,操作系統層面或者軟件層面來實現。在 Linux 平臺上 ZGC 采用了多重映射(Mult-Mapping)將多個不同的虛擬內存地址映射到同一個物理內存地址上,著是一種多對一映射,一位著 ZGC 在虛擬內中看到的地址空間要比時機的堆內存容量來得更大。把染色指針中的標志位看作是地址分段符,那只要將這些不同的地址分段符都映射到同一個福利內空間,經過多重映射轉換后,就可以直接使用染色指針進行尋址了,如下圖所示:
多重映射技術確實可能帶來一些諸如復制大對象時會更容易這樣額外的好處,但是從源頭上來說,ZGC
的多重映射只是采用染色指針的衍生品,并不是為了專門的為實現其他某種特征需求而做的。
讀屏障
ZGC采用的讀屏障的方式來修正指針引用,由于ZGC采用的是復制整理的方式進行GC,很有可能在對象的位置改變之后指針位置尚未更新時程序調用了該對象,那么此時在程序需要并行的獲取該對象的引用時,ZGC就會對該對象的指針進行讀取,判斷Remapped標識,如果標識為該對象位于本次需要清理的region區中,該對象則會有內存地址變化,會在指針中將新的引用地址替換原有對象的引用地址,然后再進行返回。
如此,使用讀屏障便解決了并發GC的對象讀取問題。
Object o = obj.fieldA; // Loading an object reference from heap
<load barrier needed here>
Object p = o; // No barrier, not a load from heap
o.doSomething(); // No barrier, not a load from heap
int i = obj.fieldB; // No barrier, not an object reference
LoadBarriers的存在,所以會導致配置ZGC的應用的吞吐量會變低。官方的測試數據是需要多出額外4%的開銷:
ZGC 工作過程
ZGC 的運作過程主要可以分為以下四個階段:
ZGC 處理過程.png
并發標記(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維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關系。得益于染色指針的支持,ZGC收集器能僅從引用上就明確得知一個對象是否處于重分配集之中,如果用戶線程此時并發訪問了位于重分配集中的對象,這次訪問將會被預置的內存屏障所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。
這樣做的好處是只有第一次訪問舊對象會陷入轉發,也就是只慢一次,對比 Shenandoah 的 Brooks 轉發指針,那是每次對象訪問都必須付出的固定開銷,簡單地說就是每 次都慢,因此 ZGC 對用戶程序的運行時負載要 Shenandoah 來得更低一些。還有另外一個直接的好處是由于染色指針的存在,一旦重分配集中某個 Region 的存活對象都復制完畢后,這個 Region 就可以立即釋放用于新對象的分配(但是轉發表還得留著不能釋放掉),哪怕堆中還有很多指向這個對象的未更新指針也沒有關系,這些舊指針一旦被使用,它們都是可以自愈的。
并發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用,這一點從目標角度看是與 Shenandoah 并發引用更新階段一樣的,但是 ZGC 的并發重映射并不是一個必須要“迫切”去完成的任務,因為前面說過,即使是舊引用,它也是可以自愈的,最多只是第一次使用時多一次轉發和修正操作。重映射清理這些舊引用的主要目的是為了不變慢(還有清理結束后可以釋放轉發表這樣的附帶收益),所以說這并不是很“迫切”。因此,ZGC 很巧妙地把并發重映射階段要做的工作,合并到了下一次垃圾收集循環中的并發標記階段里去完成,反正它們都是要遍歷所有對象的,這樣合并就節省了一次遍歷對象的開銷。一旦所有指針都被修正之后,原來記錄新舊對象關系的轉發表就可以釋放掉了。
ZGC 核心參數
ZGC 觸發時機
ZGC 中的幾種觸發GC場景:
- 定時觸發:默認為不使用,可以通過 ZCollectionInterval 參數配置。GC 日志中的關鍵字 “Timer”。
- 預熱觸發:最多三次,在堆內存空間達到 10%、20%、30% 時機觸發、主要是通過 GC 的時間、為其他的 GC 觸發準備。GC日志關鍵字 “Warmup”。
- 分配速率:基于正態分布統計,計算內存 99% 可能的最大分配速率,以及此速率下內存將要耗盡的時間點,在耗盡之前觸發 GC (耗盡時間,一次 GC 最大持續時間-一次 GC 檢測周期時間)。GC日志關鍵字 “Allocation Rate”。
- 主動觸發:(默認開啟,可以通過 ZProactictive 參數配置)距上一次 GC 堆內存增長 10%,超過 5 分鐘時,對比上次 GC的間隔時間限(一次 GC 最大持續時間),超過則觸發。GC 日志關鍵字 “Proactive”。
- 元數據分配觸發:元數據區不足導致,GC 日志關鍵中是 “Metadata GC Threshold”
- 直接觸發:代碼中顯示調用 System.gc() 觸發,GC 日志關鍵字是 “System.gc()”。
- 阻塞內存分配請求觸發:垃圾對象來不及揮手,占滿整個堆空間,導致部分線程阻塞,GC 日志關鍵字是 “Allocation Stall”。
ZGC 日志分析
我們將對下面的一個簡單的程序做一個 ZGC LOG 做一個分析,下面是具體的代碼和分析。
示例代碼
下面是一段簡單的代碼:
/**
* VM Args:-XX:+UseZGC -Xmx8m -Xlog:gc*
*/
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[2048]);
}
}
}
GC 日志分析
GC 日志如下(運行環境 JDK 17),舉個例子:
GC 日志中每一行都標注了對 GC 過程中的信息,關鍵信息如下:
- Start:開始GC,并標明的GC觸發的原因。上圖中觸發原因是自適應算法。
- Phase-Pause Mark Start:初始標記,會STW。
- Phase-Pause Mark End:再次標記,會STW。
- Phase-Pause Relocate Start:初始轉移,會STW。
Heap信息:記錄了GC過程中Mark、Relocate前后的堆大小變化狀況。High和Low記錄了其中的最大值和最小值,我們一般關注High中Used的值,如果達到100%,在GC過程中一定存在內存分配不足的情況,需要調整GC的觸發時機,更早或者更快地進行GC。
GC信息統計:可以定時的打印垃圾收集信息,觀察10秒內、10分鐘內、10個小時內,從啟動到現在的所有統計信息。利用這些統計信息,可以排查定位一些異常點。
ZGC 總結
本文主要是從概念上描述了 ZGC 的特征和工作過程。
目前大多數互聯網公司還是使用 jdk 8、jdk 11 主流使用的還是 ParNew + CMS 組合或者 G1。
對于我們一線 Java 開發者應該具備新技術的學習熱情和關注度,才能在激烈的社會競爭中保持優勢。
參考資料
- 深入理解 JVM 虛擬機第三版 周志明。
- https://wiki.openjdk.java.net/display/zgc/Main。
- http://cr.openjdk.java.net/~pliden/slides/ZGC-Jfokus-2018.pdf。
- https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html。