JVM 從入門到放棄之 ZGC 垃圾收集器

ZGC 概述

Z Garbage Collector,也稱爲 ZGC,在 jdk 11 中引入的一種可擴展的低延遲垃圾收集器,在 jdk 15 中發佈穩定版。在旨在滿足以下目標:

ZGC 具有以下特徵:

ZGC 的核心是一個併發垃圾收集器,這意味着所有繁重的工作都在 Java 線程繼續執行的同時完成。這極大地限制了垃圾收集對應用程序響應時間的影響。

ZGC 特徵

ZGC 收集器是一款基於 Region 內存佈局的,(暫時) 不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記 - 整理算法的,以低延遲爲首要目標的一款垃圾收集器。

內存佈局

ZGC 沒有分代的概念

ZGC 的內存佈局說起。與 Shenandoah 和 G1 一樣,ZGC 也採用基於 Region 的堆內存佈局,但與它們不同的是 , ZGC 的 Region 具 有 動 態 性 (動態創建和銷燬 , 以及動態的區域容量大小)。在 x64 硬件平臺下 , ZGC 的 Region 可以具有大、中、小三類容量 (如下圖所示):

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 位被分爲:

爲什麼會有兩個 mark 標記?

每一個 GC 週期開始時,會交換使用的標記位,使上次 GC 週期中修正的已標記狀態失效,所有引用都變成未標記。GC 週期 1:使用 mark0, 則週期結束所有引用 mark 標記都會成爲 01。GC 週期 2:使用 mark1, 與週期 1 相同,所有的 mark 標記都會成爲 10。

ZGC 不能做指針壓縮?

指針壓縮指的是壓縮爲 32 位,尋址位數不能超過 35,也就是 JVM 內存最大爲 32G(2^35=32GB),這裏的尋址位數已經達到了 42 位。

顏色指針的三大優勢 ?

  1. 在一個 Region 中的所有存活對象都被移走後 (複製走後),這個 Region 就可以被立即釋放掉,因爲它還有轉發表記錄着原始地址和新地址,這樣的話,理論上,只要還有一個 Region 對象空閒,ZGC 就能完成垃圾收集。

  2. 顏色指針有指針的 “自愈”(Self-Healing)能力,這樣子就減少了寫屏障(例如三色標記中的增量更新或原始快照),只需要一個讀屏障就可以解決問題,減少了內存屏障的使用數量。

  3. 顏色指針有着極大的擴展性,因爲還有 18 位未使用,這樣更有利於後續功能的擴展。

多重映射尋址

不同的虛擬機內存到物理內存的轉換關係可以在硬件層面,操作系統層面或者軟件層面來實現。在 Linux 平臺上 ZGC 採用了多重映射(Mult-Mapping)將多個不同的虛擬內存地址映射到同一個物理內存地址上,着是一種多對一映射,一位着 ZGC 在虛擬內中看到的地址空間要比時機的堆內存容量來得更大。把染色指針中的標誌位看作是地址分段符,那隻要將這些不同的地址分段符都映射到同一個福利內空間,經過多重映射轉換後,就可以直接使用染色指針進行尋址了,如下圖所示:多重映射技術確實可能帶來一些諸如複製大對象時會更容易這樣額外的好處,但是從源頭上來說,ZGC 的多重映射只是採用染色指針的衍生品,並不是爲了專門的爲實現其他某種特徵需求而做的。

讀屏障

ZGC 採用的讀屏障的方式來修正指針引用,由於 ZGC 採用的是複製整理的方式進行 GC,很有可能在對象的位置改變之後指針位置尚未更新時程序調用了該對象,那麼此時在程序需要並行的獲取該對象的引用時,ZGC 就會對該對象的指針進行讀取,判斷 Remapped 標識,如果標識爲該對象位於本次需要清理的 region 區中,該對象則會有內存地址變化,會在指針中將新的引用地址替換原有對象的引用地址,然後再進行返回。

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

如此,使用讀屏障便解決了併發 GC 的對象讀取問題,

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 核心參數

qa5T5E

ZGC 觸發時機

ZGC 中的幾種觸發 GC 場景:

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 過程中的信息,關鍵信息如下:

ZGC 總結

  1. 本文主要是從概念上描述了 ZGC 的特徵和工作過程。

  2. 目前大多數互聯網公司還是使用 jdk 8、jdk 11 主流使用的還是 ParNew + CMS 組合或者 G1

  3. 對於我們一線 Java 開發者應該具備新技術的學習熱情和關注度,才能在激烈的社會競爭中保持優勢。

參考資料

公衆號:運維開發故事

github:https://github.com/orgs/sunsharing-note/dashboard

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UY6-5GTPhjjDZFpe5RV7_g