12 張圖帶你徹底理解 ZGC

大家好,我是君哥。今天來聊一聊 ZGC。

ZGC(Z Garbage Collector) 是一款性能比 G1 更加優秀的垃圾收集器。ZGC 第一次出現是在  JDK 11 中以實驗性的特性引入,這也是 JDK 11 中最大的亮點。在 JDK 15 中 ZGC 不再是實驗功能,可以正式投入生產使用了,使用 –XX:+UseZGC 可以啓用 ZGC。

ZGC 有 3 個重要特性:

JDK 16 發佈後,GC 暫停時間已經縮小到 1 ms 以內,並且時間複雜度是 o(1),這也就是說 GC 停頓時間是一個固定值了,並不會受堆內存大小影響。

下面圖片來自: https://malloc.se/blog/zgc-jdk16

1 內存多重映射

內存多重映射,就是使用 mmap 把不同的虛擬內存地址映射到同一個物理內存地址上。如下圖:

ZGC 爲了更靈活高效地管理內存,使用了內存多重映射,把同一塊兒物理內存映射爲 Marked0、Marked1 和 Remapped 三個虛擬內存。

當應用程序創建對象時,會在堆上申請一個虛擬地址,這時 ZGC 會爲這個對象在 Marked0、Marked1 和 Remapped 這三個視圖空間分別申請一個虛擬地址,這三個虛擬地址映射到同一個物理地址。

Marked0、Marked1 和 Remapped 這三個虛擬內存作爲 ZGC 的三個視圖空間,在同一個時間點內只能有一個有效。ZGC 就是通過這三個視圖空間的切換,來完成併發的垃圾回收。

2 染色指針

2.1 三色標記回顧

我們知道 G1 垃圾收集器使用了三色標記,這裏先做一個回顧。下面是一個三色標記過程中的對象引用示例圖:

總共有三種顏色,說明如下:

三色標記的過程如下:

  1. 初始階段,所有對象都是白色。

  2. 將 GC Roots 直接引用的對象標記爲灰色。

  3. 處理灰色對象,把當前灰色對象引用的所有對象都變成灰色,之後將當前灰色對象變成黑色。

  4. 重複步驟 3,直到不存在灰色對象爲止。

三色標記結束後,白色對象就是沒有被引用的對象(比如上圖中的 H  和 G),可以被回收了。

2.2 染色指針

ZGC 出現之前, GC 信息保存在對象頭的 Mark Word 中。比如 64 位的 JVM,對象頭的 Mark Word 中保存的信息如下圖:

前 62 位保存了 GC 信息,最後兩位保存了鎖標誌。

ZGC 的一大創舉是將 GC 信息保存在了染色指針上。染色指針是一種將少量信息直接存儲在指針上的技術。在 64 位 JVM  中,對象指針是 64 位,如下圖:

在這個 64 位的指針上,高 16 位都是 0,暫時不用來尋址。剩下的 48 位支持的內存可以達到 256 TB(2 ^48), 這可以滿足多數大型服務器的需要了。不過 ZGC 並沒有把 48 位都用來保存對象信息,而是用高 4 位保存了四個標誌位,這樣 ZGC 可以管理的最大內存可以達到 16 TB(2 ^ 44)。

通過這四個標誌位,JVM 可以從指針上直接看到對象的三色標記狀態(Marked0、Marked1)、是否進入了重分配集(Remapped)、是否需要通過 finalize 方法來訪問到(Finalizable)。

無需進行對象訪問就可以獲得 GC 信息,這大大提高了 GC 效率。

3 內存佈局

首先我們回顧一下 G1 垃圾收集器的內存佈局。G1 把整個堆分成了大小相同的 region,每個堆大約可以有 2048 個 region,每個 region 大小爲 1~32 MB (必須是 2 的次方)。如下圖:

跟 G1 類似,ZGC 的堆內存也是基於 Region 來分佈,不過 ZGC 是不區分新生代老年代的。不同的是,ZGC 的 Region 支持動態地創建和銷燬,並且 Region 的大小不是固定的,包括三種類型的 Region :

4 讀屏障

讀屏障類似於 Spring AOP 的前置增強,是 JVM 嚮應用代碼中插入一小段代碼,當應用線程從堆中讀取對象的引用時,會先執行這段代碼。注意:只有從堆內存中讀取對象的引用時,纔會執行這個代碼。下面代碼只有第一行需要加入讀屏障。

Object o = obj.FieldA
Object p = o //不是從堆中讀取引用
o.dosomething() //不是從堆中讀取引用
int i =  obj.FieldB //不是引用類型

讀屏障在解釋執行時通過 load 相關的字節碼指令加載數據。作用是在對象標記和轉移過程中,判斷對象的引用地址是否滿足條件,並作出相應動作。如下圖:

標記、轉移和重定位這些過程請看下一節。

讀屏障會對應用程序的性能有一定影響,據測試,對性能的最高影響達到 4%,但提高了 GC 併發能力,降低了 STW。

5 GC 過程

前面已經講過,ZGC 使用內存多重映射技術,把物理內存映射爲 Marked0、Marked1 和 Remapped 三個地址視圖,利用地址視圖的切換,ZGC 實現了高效的併發收集。

ZGC 的垃圾收集過程包括標記、轉移和重定位三個階段。如下圖:

ZGC 初始化後,整個內存空間的地址視圖被設置爲 Remapped。

5.1 初始標記

從 GC Roots 出發,找出 GC Roots 直接引用的對象,放入活躍對象集合,這個過程需要 STW,不過 STW 的時間跟 GC Roots 數量成正比,耗時比較短。

5.2 併發標記

併發標記過程中,GC 線程和 Java 應用線程會並行運行。這個過程需要注意下面幾點:

標記階段的活躍視圖也可能是 Marked1,爲什麼會採用兩個視圖呢?

這裏採用兩個視圖是爲了區分前一次標記和這一次標記。如果這次標記的視圖是 Marked0,那下一次併發標記就會把視圖切換到 Marked1。這樣做可以配合 ZGC 按照頁回收垃圾的做法。如下圖:

第二次標記的時候,如果還是切換到 Marked0,那麼 2 這個對象區分不出是活躍的還是上次標記過的。如果第二次標記切換到 Marked1,就可以區分出了。

這時 Marked0 這個視圖的對象就是上次標記過程被標記過活躍,轉移的時候沒有被轉移,但這次標記沒有被標記爲活躍的對象。Marked1 視圖的對象是這次標記被標記爲活躍的對象。Remapped 視圖的對象是上次垃圾回收發生轉移或者是被 Java 應用線程訪問過,本次垃圾回收中被標記爲不活躍的對象。

5.3 再標記

併發標記階段 GC 線程和 Java 應用線程併發執行,標記過程中可能會有引用關係發生變化而導致的漏標記問題。再標記階段重新標記併發標記階段發生變化的對象,還會對非強引用(軟應用,虛引用等)進行並行標記。

這個階段需要 STW,但是需要標記的對象少,耗時很短。

5.4 初始轉移

轉移就是把活躍對象複製到新的內存,之前的內存空間可以被回收。

初始轉移需要掃描 GC Roots 直接引用的對象並進行轉移,這個過程需要 STW,STW 時間跟 GC Roots 成正比。

5.5 併發轉移

併發轉移過程 GC 線程和 Java 線程是併發進行的。上面已經講過,轉移過程中對象視圖會被切回 Remapped 。轉移過程需要注意以下幾點:

5.6 重定位

轉移過程對象的地址發生了變化,在這個階段,把所有指向對象舊地址的指針調整到對象的新地址上。

6 垃圾收集算法

ZGC 採用標記 - 整理算法,算法的思想是把所有存活對象移動到堆的一側,移動完成後回收掉邊界以外的對象。如下圖:

4.1 JDK 16 之前

在 JDK 16 之前,ZGC 會預留(Reserve)一塊兒堆內存,這個預留內存不能用於 Java 線程的內存分配。即使從 Java 線程的角度看堆內存已經滿了也不能使用 Reserve,只有 GC 過程中搬移存活對象的時候纔可以使用。如下圖:

這樣做的好處是算法簡單,非常適合並行收集。但這樣做有幾個問題:

4.2 JDK 16 改進

JDK 16 發佈後,ZGC 支持就地搬移對象(G1 在 Full GC 的時候也是就地搬移)。這樣做的好處是不用預留空閒內存了。如下圖:

不過就地搬移也有一定的挑戰。比如:必須考慮搬移對象的順序,否則可能會覆蓋尚未移動的對象。這就需要 GC 線程之間更好的進行協作,不利於併發收集,同時也會導致搬移對象的 Java 線程需要考慮什麼可以做什麼不可以做。

爲了獲得更好的 GC 表現,JDK 16 在支持就地搬移的同時,也支持預留(Reserve)堆內存的方式,並且 ZGC 不需要真的預留空閒的堆內存。默認情況下,只要有空閒的 region,ZGC 就會使用預留堆內存的方式,如果沒有空閒的 region,否則 ZGC 就會啓用就地搬移。如果有了空閒的 region, ZGC 又會切換到預留堆內存的搬移方式。

7 總結

內存多重映射和染色指針的引入,使 ZGC 的併發性能大幅度提升。

ZGC 只有 3 個需要 STW 的階段,其中初始標記和初始轉移只需要掃描所有 GC Roots,STW 時間 GC Roots 的數量成正比,不會耗費太多時間。再標記過程主要處理併發標記引用地址發生變化的對象,這些對象數量比較少,耗時非常短。可見整個 ZGC 的 STW 時間幾乎只跟 GC Roots 數量有關係,不會隨着堆大小和對象數量的變化而變化。

ZGC 也有一個缺點,就是浮動垃圾。因爲 ZGC 沒有分代概念,雖然 ZGC 的 STW 時間在 1ms 以內,但是 ZGC 的整個執行過程耗時還是挺長的。在這個過程中 Java 線程可能會創建大量的新對象,這些對象會成爲浮動垃圾,只能等下次 GC 的時候進行回收。

參考:

1.https://wiki.openjdk.java.net/display/zgc

2.https://openjdk.java.net/jeps/304

3.https://openjdk.java.net/jeps/376

4.https://malloc.se/blog/zgc-jdk16

5.https://mp.weixin.qq.com/s/ag5u2EPObx7bZr7hkcrOTg

6.https://mp.weixin.qq.com/s/FIr6r2dcrm1pqZj5Bubbmw

7.https://www.jianshu.com/p/664e4da05b2c

8.https://www.cnblogs.com/jimoer/p/13170249.html

9.https://www.jianshu.com/p/12544c0ad5c1

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