自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

12 張圖帶你徹底理解 ZGC

存儲(chǔ) 存儲(chǔ)設(shè)備
ZGC(Z Garbage Collector) 是一款性能比 G1 更加優(yōu)秀的垃圾收集器。ZGC 的一大創(chuàng)舉是將 GC 信息保存在了染色指針上。染色指針是一種將少量信息直接存儲(chǔ)在指針上的技術(shù)。

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

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

ZGC 有 3 個(gè)重要特性:

  • 暫停時(shí)間不會(huì)超過 10 ms。

JDK 16 發(fā)布后,GC 暫停時(shí)間已經(jīng)縮小到 1 ms 以內(nèi),并且時(shí)間復(fù)雜度是 o(1),這也就是說 GC 停頓時(shí)間是一個(gè)固定值了,并不會(huì)受堆內(nèi)存大小影響。

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

  • 最大支持 16TB 的大堆,最小支持 8MB 的小堆。
  • 跟 G1 相比,對應(yīng)用程序吞吐量的影響小于 15 %。

1.內(nèi)存多重映射

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

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

當(dāng)應(yīng)用程序創(chuàng)建對象時(shí),會(huì)在堆上申請一個(gè)虛擬地址,這時(shí) ZGC 會(huì)為這個(gè)對象在 Marked0、Marked1 和 Remapped 這三個(gè)視圖空間分別申請一個(gè)虛擬地址,這三個(gè)虛擬地址映射到同一個(gè)物理地址。

Marked0、Marked1 和 Remapped 這三個(gè)虛擬內(nèi)存作為 ZGC 的三個(gè)視圖空間,在同一個(gè)時(shí)間點(diǎn)內(nèi)只能有一個(gè)有效。ZGC 就是通過這三個(gè)視圖空間的切換,來完成并發(fā)的垃圾回收。

2.染色指針

2.1 三色標(biāo)記回顧

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

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

  • 白色:本對象還沒有被標(biāo)記線程訪問過。
  • 灰色:本對象已經(jīng)被訪問過,但是本對象引用的其他對象還沒有被全部訪問。
  • 黑色:本對象已經(jīng)被訪問過,并且本對象引用的其他對象也都被訪問過了。

三色標(biāo)記的過程如下:

  • 初始階段,所有對象都是白色。
  • 將 GC Roots 直接引用的對象標(biāo)記為灰色。
  • 處理灰色對象,把當(dāng)前灰色對象引用的所有對象都變成灰色,之后將當(dāng)前灰色對象變成黑色。
  • 重復(fù)步驟 3,直到不存在灰色對象為止。

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

2.2 染色指針

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

前 62位保存了 GC 信息,最后兩位保存了鎖標(biāo)志。

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

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

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

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

3.內(nèi)存布局

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

  • 跟 G1 類似,ZGC 的堆內(nèi)存也是基于 Region 來分布,不過 ZGC 是不區(qū)分新生代老年代的。不同的是,ZGC 的 Region 支持動(dòng)態(tài)地創(chuàng)建和銷毀,并且 Region 的大小不是固定的,包括三種類型的 Region :
  • Small Region:2MB,主要用于放置小于 256 KB 的小對象。
  • Medium Region:32MB,主要用于放置大于等于 256 KB 小于 4 MB 的對象。
  • Large Region:N * 2MB。這個(gè)類型的 Region 是可以動(dòng)態(tài)變化的,不過必須是 2MB 的整數(shù)倍,最小支持 4 MB。每個(gè) Large Region 只放置一個(gè)大對象,并且是不會(huì)被重分配的。

4.讀屏障

讀屏障類似于 Spring AOP 的前置增強(qiáng),是 JVM 向應(yīng)用代碼中插入一小段代碼,當(dāng)應(yīng)用線程從堆中讀取對象的引用時(shí),會(huì)先執(zhí)行這段代碼。注意:只有從堆內(nèi)存中讀取對象的引用時(shí),才會(huì)執(zhí)行這個(gè)代碼。下面代碼只有第一行需要加入讀屏障。

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




讀屏障在解釋執(zhí)行時(shí)通過 load 相關(guān)的字節(jié)碼指令加載數(shù)據(jù)。作用是在對象標(biāo)記和轉(zhuǎn)移過程中,判斷對象的引用地址是否滿足條件,并作出相應(yīng)動(dòng)作。如下圖:

標(biāo)記、轉(zhuǎn)移和重定位這些過程請看下一節(jié)。

讀屏障會(huì)對應(yīng)用程序的性能有一定影響,據(jù)測試,對性能的最高影響達(dá)到 4%,但提高了 GC 并發(fā)能力,降低了 STW。

5.GC 過程

前面已經(jīng)講過,ZGC 使用內(nèi)存多重映射技術(shù),把物理內(nèi)存映射為 Marked0、Marked1 和 Remapped 三個(gè)地址視圖,利用地址視圖的切換,ZGC 實(shí)現(xiàn)了高效的并發(fā)收集。

ZGC 的垃圾收集過程包括標(biāo)記、轉(zhuǎn)移和重定位三個(gè)階段。如下圖:

ZGC 初始化后,整個(gè)內(nèi)存空間的地址視圖被設(shè)置為 Remapped。

5.1 初始標(biāo)記

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

5.2 并發(fā)標(biāo)記

并發(fā)標(biāo)記過程中,GC 線程和 Java 應(yīng)用線程會(huì)并行運(yùn)行。這個(gè)過程需要注意下面幾點(diǎn):

  • GC 標(biāo)記線程訪問對象時(shí),如果對象地址視圖是 Remapped,就把對象地址視圖切換到 Marked0,如果對象地址視圖已經(jīng)是 Marked0,說明已經(jīng)被其他標(biāo)記線程訪問過了,跳過不處理。
  • 標(biāo)記過程中Java 應(yīng)用線程新創(chuàng)建的對象會(huì)直接進(jìn)入 Marked0 視圖。
  • 標(biāo)記過程中Java 應(yīng)用線程訪問對象時(shí),如果對象的地址視圖是 Remapped,就把對象地址視圖切換到 Marked0,可以參考前面講的讀屏障。
  • 標(biāo)記結(jié)束后,如果對象地址視圖是 Marked0,那就是活躍的,如果對象地址視圖是 Remapped,那就是不活躍的。

標(biāo)記階段的活躍視圖也可能是 Marked1,為什么會(huì)采用兩個(gè)視圖呢?

這里采用兩個(gè)視圖是為了區(qū)分前一次標(biāo)記和這一次標(biāo)記。如果這次標(biāo)記的視圖是 Marked0,那下一次并發(fā)標(biāo)記就會(huì)把視圖切換到 Marked1。這樣做可以配合 ZGC 按照頁回收垃圾的做法。如下圖:

第二次標(biāo)記的時(shí)候,如果還是切換到 Marked0,那么 2 這個(gè)對象區(qū)分不出是活躍的還是上次標(biāo)記過的。如果第二次標(biāo)記切換到 Marked1,就可以區(qū)分出了。

這時(shí) Marked0 這個(gè)視圖的對象就是上次標(biāo)記過程被標(biāo)記過活躍,轉(zhuǎn)移的時(shí)候沒有被轉(zhuǎn)移,但這次標(biāo)記沒有被標(biāo)記為活躍的對象。Marked1 視圖的對象是這次標(biāo)記被標(biāo)記為活躍的對象。Remapped 視圖的對象是上次垃圾回收發(fā)生轉(zhuǎn)移或者是被 Java 應(yīng)用線程訪問過,本次垃圾回收中被標(biāo)記為不活躍的對象。

5.3 再標(biāo)記

并發(fā)標(biāo)記階段 GC 線程和 Java 應(yīng)用線程并發(fā)執(zhí)行,標(biāo)記過程中可能會(huì)有引用關(guān)系發(fā)生變化而導(dǎo)致的漏標(biāo)記問題。再標(biāo)記階段重新標(biāo)記并發(fā)標(biāo)記階段發(fā)生變化的對象,還會(huì)對非強(qiáng)引用(軟應(yīng)用,虛引用等)進(jìn)行并行標(biāo)記。

這個(gè)階段需要 STW,但是需要標(biāo)記的對象少,耗時(shí)很短。

5.4 初始轉(zhuǎn)移

轉(zhuǎn)移就是把活躍對象復(fù)制到新的內(nèi)存,之前的內(nèi)存空間可以被回收。

初始轉(zhuǎn)移需要掃描 GC Roots 直接引用的對象并進(jìn)行轉(zhuǎn)移,這個(gè)過程需要 STW,STW 時(shí)間跟 GC Roots 成正比。

5.5 并發(fā)轉(zhuǎn)移

并發(fā)轉(zhuǎn)移過程 GC 線程和 Java 線程是并發(fā)進(jìn)行的。上面已經(jīng)講過,轉(zhuǎn)移過程中對象視圖會(huì)被切回 Remapped 。轉(zhuǎn)移過程需要注意以下幾點(diǎn):

如果 GC 線程訪問對象的視圖是 Marked0,則轉(zhuǎn)移對象,并把對象視圖設(shè)置成 Remapped。

如果 GC 線程訪問對象的視圖是 Remapped,說明被其他 GC 線程處理過,跳過不再處理。

并發(fā)轉(zhuǎn)移過程中 Java 應(yīng)用線程創(chuàng)建的新對象地址視圖是 Remapped。

如果 Java 應(yīng)用線程訪問的對象被標(biāo)記為活躍并且對象視圖是 Marked0,則轉(zhuǎn)移對象,并把對象視圖設(shè)置成 Remapped。

5.6 重定位

轉(zhuǎn)移過程對象的地址發(fā)生了變化,在這個(gè)階段,把所有指向?qū)ο笈f地址的指針調(diào)整到對象的新地址上。

6.垃圾收集算法

ZGC 采用標(biāo)記 - 整理算法,算法的思想是把所有存活對象移動(dòng)到堆的一側(cè),移動(dòng)完成后回收掉邊界以外的對象。如下圖:

6.1 JDK 16 之前

在 JDK 16 之前,ZGC 會(huì)預(yù)留(Reserve)一塊兒堆內(nèi)存,這個(gè)預(yù)留內(nèi)存不能用于 Java 線程的內(nèi)存分配。即使從 Java 線程的角度看堆內(nèi)存已經(jīng)滿了也不能使用 Reserve,只有 GC 過程中搬移存活對象的時(shí)候才可以使用。如下圖:

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

因?yàn)橛蓄A(yù)留內(nèi)存,能給 Java 線程分配的堆內(nèi)存小于 JVM 聲明的堆內(nèi)存。

Reserve 僅僅用于存放 GC 過程中搬移的對象,有點(diǎn)內(nèi)存浪費(fèi)。

因?yàn)?Reserve 不能給 GC 過程中搬移對象的 Java 線程使用,搬移線程可能會(huì)因?yàn)樯暾埐坏阶銐騼?nèi)存而不能完成對象搬移,這返回過來又會(huì)導(dǎo)致應(yīng)用程序的 OOM。

6.2 JDK 16 改進(jìn)

JDK 16 發(fā)布后,ZGC 支持就地搬移對象(G1 在 Full GC 的時(shí)候也是就地搬移)。這樣做的好處是不用預(yù)留空閑內(nèi)存了。如下圖:

不過就地搬移也有一定的挑戰(zhàn)。比如:必須考慮搬移對象的順序,否則可能會(huì)覆蓋尚未移動(dòng)的對象。這就需要 GC 線程之間更好的進(jìn)行協(xié)作,不利于并發(fā)收集,同時(shí)也會(huì)導(dǎo)致搬移對象的 Java 線程需要考慮什么可以做什么不可以做。

為了獲得更好的 GC 表現(xiàn),JDK 16 在支持就地搬移的同時(shí),也支持預(yù)留(Reserve)堆內(nèi)存的方式,并且 ZGC 不需要真的預(yù)留空閑的堆內(nèi)存。默認(rèn)情況下,只要有空閑的 region,ZGC 就會(huì)使用預(yù)留堆內(nèi)存的方式,如果沒有空閑的 region,否則 ZGC 就會(huì)啟用就地搬移。如果有了空閑的 region, ZGC 又會(huì)切換到預(yù)留堆內(nèi)存的搬移方式。

7.總結(jié)

內(nèi)存多重映射和染色指針的引入,使 ZGC 的并發(fā)性能大幅度提升。

ZGC 只有 3 個(gè)需要 STW 的階段,其中初始標(biāo)記和初始轉(zhuǎn)移只需要掃描所有 GC Roots,STW 時(shí)間 GC Roots 的數(shù)量成正比,不會(huì)耗費(fèi)太多時(shí)間。再標(biāo)記過程主要處理并發(fā)標(biāo)記引用地址發(fā)生變化的對象,這些對象數(shù)量比較少,耗時(shí)非常短??梢娬麄€(gè) ZGC 的 STW 時(shí)間幾乎只跟 GC Roots 數(shù)量有關(guān)系,不會(huì)隨著堆大小和對象數(shù)量的變化而變化。

ZGC 也有一個(gè)缺點(diǎn),就是浮動(dòng)垃圾。因?yàn)?ZGC 沒有分代概念,雖然 ZGC 的 STW 時(shí)間在 1ms 以內(nèi),但是 ZGC 的整個(gè)執(zhí)行過程耗時(shí)還是挺長的。在這個(gè)過程中 Java 線程可能會(huì)創(chuàng)建大量的新對象,這些對象會(huì)成為浮動(dòng)垃圾,只能等下次 GC 的時(shí)候進(jìn)行回收。

責(zé)任編輯:武曉燕 來源: 君哥聊技術(shù)
相關(guān)推薦

2022-07-11 11:06:11

RocketMQ函數(shù).消費(fèi)端

2022-07-04 11:06:02

RocketMQ事務(wù)消息實(shí)現(xiàn)

2021-05-18 06:55:07

Java AQS源碼

2021-12-06 07:15:47

Pulsar地域復(fù)制

2020-10-16 06:30:45

分布式場景方案

2020-11-27 06:28:55

Spring循環(huán)依賴

2021-08-15 18:59:13

垃圾收集器JDK

2020-11-13 10:29:37

流程控制語句

2022-04-11 11:55:34

架構(gòu)技術(shù)調(diào)優(yōu)

2022-06-11 18:15:26

KubernetesDockerLinux

2022-06-13 11:05:35

RocketMQ消費(fèi)者線程

2023-04-11 08:35:22

RocketMQ云原生

2024-07-03 08:28:44

HWKafkaLEO

2021-04-25 10:45:59

Docker架構(gòu)Job

2020-11-03 10:32:48

回調(diào)函數(shù)模塊

2021-10-22 09:28:15

開發(fā)技能代碼

2022-06-27 11:04:24

RocketMQ順序消息

2019-07-24 08:49:36

Docker容器鏡像

2022-10-20 08:31:33

加鎖解鎖代碼

2022-05-09 11:15:05

RocketMQPULL 模式PUSH 模式
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號