從歷代GC演算法角度刨析ZGC

語言: CN / TW / HK

作者:京東科技 文濤

 

前言

本文所有介紹僅限於HotSpot虛擬機器,
本文先介紹了垃圾回收的必要手段,基於這些手段講解了歷代垃圾回收演算法是如何工作的,
每一種演算法不會講的特別詳細,只為讀者從演算法角度理解工作原理,從而引出ZGC,方便讀者循序漸進地瞭解。

GC是Garbage Collection的縮寫,顧名思義垃圾回收機制,即當需要分配的記憶體空間不再使用的時候,JVM將呼叫垃圾回收機制來回收記憶體空間。

那麼JVM的垃圾機制是如何工作的呢?

第一步識別出哪些空間不再使用(識別並標記出哪些物件已死);

第二步回收不再使用空間(清除已死物件 )

判斷物件是否已死

判斷物件是否已死通常有兩種方式 ,引用計數法和可達性分析法

引用計數法

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1:當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不能再被使用的。

簡單高效,但無法解決迴圈引用問題,a=b,b=a

引用計數法並沒有在產品級的JVM中得到應用

可達性分析法

這個演算法的基本思路就是通過一系列的稱為“ GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈( Reference Chain),當一個物件到 GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從 GC Roots到這個物件不可達)時,則證明此物件是不可用的。



不過可達性演算法中的物件並不是立即死亡的,物件擁有一次自我拯救的機會,物件被系統宣告死亡至少要經歷兩次標記過程,第一次是經過可達性分析之後沒有與GC Roots相連的引用鏈,第二次是在由虛擬機器自動建立的Finalize佇列中判斷是否需要執行finalize()方法。

HotSopt虛擬機器採用該演算法。

清除已死物件的方式

標記清除演算法

先標記再清除

不足:1 效率問題,標記和清除效率都不高。2 空間問題,產生大量空間碎片

複製演算法

記憶體分兩塊,A,B

A用完了,將存活物件拷貝到B,A清理掉

代價:記憶體少了一半。

HotSopt虛擬機器用此演算法回收新生代。將新生代記憶體劃分為8:1:1的Eden和Survivor解決複製演算法記憶體使用率低的問題



標記整理演算法

老年代使用,方式和標記清除類似,只是不直接清除,而是將後續物件向一端移動,並清理掉邊界以外的記憶體。



分代收集演算法

分代收集是一個演算法方案,整合了以上演算法的優點,一般是把Java堆分為新生代和老年代,在新生代中,使用複製演算法老年代“標記一清理”或者“標記一整理”

歷代垃圾收集器簡介

通過上文我們瞭解了怎樣識別垃圾,怎樣清理垃圾,接下來,講ZGC之前,我們回顧一下歷代垃圾回收是怎樣做的,主要是想給讀者一種歷史的視角,任何技術都不是憑空產生的,更多的是在前人成果之上進行優化整合

我們先看一個歷代JDK垃圾收集器對比表格,以下表格著重說明或引出幾個問題:

1 CMS從來未被當作預設GC,且已廢棄

2 CMS的思想其實部分被ZGC吸收,CMS已死,但他的魂還在

3 JDK11、JDK17為長期迭代版本,專案中應優先使用這兩個版本

版本 釋出時間 預設收集器 事件
jdk1.3 2000-05-08 serial 
jdk1.4 2004-02-06 ParNew 
jdk1.5/5.0 2004-09-30 Parallel Scavenge/serial CMS登場
jdk1.6/6.0 2006-12-11 Parallel Scavenge/Parallel Old 
dk1.7/7.0 2011-07-28 Parallel Scavenge/Parallel Old G1登場
jdk1.8/8.0 2014-03-18 Parallel Scavenge/Parallel Old 
jdk1.9/9.0 2014-09-8 G1 CMS廢棄
jdk10 2018-03-21 G1 
jdk11 2018-09-25 G1 ZGC登場
jdk12 2019-3 G1 Shenandoah
jdk13 2019-9 G1 
jdk14 2020-3 G1 CMS移除
jdk15 2020-9-15 G1 ZGC、Shenandoah轉正
jdk16 2021-3-16 G1 
jdk17 2021-09-14 G1 ZGC分代
jdk18 2022-3-22 G1 
jdk19 2022-9-22 G1 

GC分類

我們經常在各種場景聽到以下幾種GC名詞,Young GC、Old GC、Mixed GC、Full GC、Major GC、Minor GC,他們到底什麼意思,本人進行了以下梳理

首先GC分兩類,Partial GC(部分回收),Full GC

Partial GC:並不收集整個GC堆的模式,以下全是Partial GC的子集

Young GC:只收集young gen的GC

Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式

Mixed GC:只有G1有這個模式,收集整個young gen以及部分old gen的GC。

Minor GC:只有G1有這個模式,收集整個young gen

Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

Major GC:通常是跟full GC是等價的

serial收集器

單執行緒收集器,“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。它依然是虛擬機器執行在 Client模式下的預設新生代收集器。它也有著優於其他收集器的地方:簡單而高效(與其他收集器的單執行緒比),對於限定單個CPU的環境來說, Serial I收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

下圖彩色部分說明了它的演算法,簡單粗暴

1 停止使用者執行緒

2 單執行緒垃圾回收新生代

3 重啟使用者執行緒



ParNew收集器

Parnew收集器其實就是 Serial l收集器的多執行緒版本。它是許多執行在 Server模式下的虛擬機器中首選的新生代收集器,其中有一個與效能無關但很重要的原因是,除了 Serial 收集器外,目前只有它能與CMS收集器配合工作。Pardew收集器在單CPU的環境中絕對不會有比 Serial收集器更好的效果。它預設開啟的收集執行緒數與CPU的數量相同,在CPU非常多(臂如32個)的環境下,可以使用-XX: ParallelGCThreads引數來限制垃圾收集的執行緒數。

ParNew收集器追求降低GC時使用者執行緒的停頓時間,適合互動式應用,良好的反應速度提升使用者體驗.

下圖彩色部分說明了它的演算法,同樣簡單粗暴

1 停止使用者執行緒

2 多執行緒垃圾回收新生代

3 重啟使用者執行緒

Parallel Scavenge 收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器。演算法的角度它和ParNew一樣,在此就不畫圖解釋了

Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量( Throughput)

吞吐量是指使用者執行緒執行時間佔CPU總時間的比例

通過以下兩種方式可達到目的:

1.在多CPU環境中使用多條GC執行緒,從而垃圾回收的時間減少,從而使用者執行緒停頓的時間也減少;

2.實現GC執行緒與使用者執行緒併發執行。

Serial Old收集器

Serial Old是 Serial 收集器的老年代版本,它同樣是一個單執行緒收集器,使用“標記整理”演算法。這個收集器的主要意義也是在於給 Client模式下的虛擬機器使用。

如果在 Server模式下,那麼它主要還有兩大用途:

一種用途是在JDK1.5以及之前的版本中與 ParallelScavenge收集器搭配使用,

另一種用途就是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

下圖彩色部分說明了它的演算法,同樣簡單粗暴

1 停止使用者執行緒

2 單執行緒垃圾回收老年代

3 重啟使用者執行緒

Parallel Old收集器

Paralle Old是 Parallel Scavenge收集器的老年代版本,一般它們搭配使用,追求CPU吞吐量,使用多執行緒和“標記一整理”演算法。

下圖彩色部分說明了它的演算法,同樣簡單粗暴

1 停止使用者執行緒

2 多執行緒垃圾回收老年代

3 重啟使用者執行緒

CMS收集器

以上5種垃圾回收原理不難理解,演算法之所以如此簡單個人理解在當時使用這種演算法就夠了,隨著JAVA的攻城略地,有一種垃圾回收需求出現,即使用盡量短的回收停頓時間,以避免過久的影響使用者執行緒,CMS登場了。

CMS( Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。

想要達到目的,就要分析GC時最佔用時間的是什麼操作,比較浪費時間的是標記已死物件、清除物件,那麼如果可以和使用者執行緒併發的進行,GC的停頓基本就限制在了標記所花費的時間。



如上圖,CMS收集器是基於“標記一清除”法實現的,它的運作過程分為4個步驟

初始標記( EMS initial mark) stop the world
併發標記( CMS concurrent mark)
重新標記( CMS remark) stop the world
併發清除( CMS concurrent sweep)

初始標記的作用是查詢GC Roots集合的過程,這個過程處理物件相對較少,速度很快。(為什麼要進行初始標記:列舉根結點。https://www.zhihu.com/question/502729840)

併發標記是實際標記所有物件是否已死的過程,比較耗時,所以採用併發的方式。

重新標記主要是處理併發標記期間所產生的新的垃圾。重新標記階段不需要再重新標記所有物件,只對併發標記階段改動過的物件做標記即可。

優點:

併發收集、低停頓

缺點:

CMS收集器對CPU資源非常敏感。

CMS收集器無法處理浮動垃圾( Floating Garbage),可能出現“Concurrent ModeFailure”失敗而導致另一次 Full GC的產生。

“標記一清除”法導致大量空間碎片產生,以至於老年代還有大量空間,卻沒有整塊空間儲存某物件。

Concurrent ModeFailure可能原因及方案
原因1:CMS觸發太晚
方案:將-XX:CMSInitiatingOccupancyFraction=N調小 (達到百分比進行垃圾回收);
原因2:空間碎片太多
方案:開啟空間碎片整理,並將空間碎片整理週期設定在合理範圍;
-XX:+UseCMSCompactAtFullCollection (空間碎片整理)
-XX:CMSFullGCsBeforeCompaction=n
原因3:垃圾產生速度超過清理速度
晉升閾值過小;
Survivor空間過小,導致溢位;
Eden區過小,導致晉升速率提高;存在大物件;

G1收集器

G1是一款面向服務端應用的垃圾收集器。下文會簡單講解一下它的“特點”和“記憶體分配與回收策略”,有基礎或不感興趣的同學直接跳到“G1垃圾回收流程”

特點

並行與併發

G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The- World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。

分代收集

與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。

空間整合

與CMS的“標記一清理”演算法不同,G1從整體來看是基於“標記一整理”演算法實現的收集器,從區域性(兩個 Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。

可預測的停頓

這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域( Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分 Region(不需要連續)的集合

記憶體分配與回收策略

物件優先在Eden分配

大多數情況下,物件在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次 Minor[ˈmaɪnə(r)] GC

大物件直接進入老年代

所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列。大物件對虛擬機器的記憶體分配來說就是一個壞訊息(比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”寫程式的時候應當避免),經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

長期存活的物件將進入老年代

虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次 Minor GC後仍然存活,並且能被 Survivor容納的話,將被移動到 Survivor空間中,並且物件年齡設為1。物件在 Survivor區中每“熬過”一次 Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設15歲)會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數據-XX : MaxTenuringThreshold設定

動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是水遠地要求物件的年齡必須達到了 MaxTenuringThreshold才能晉升老年代,如果在 Survivor空間中相同年齡所有物件大小的總和大於 Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到 MaxTenuringThreshold中要求的年齡。

空間分配擔保

在發生 Minor GC之前,虛擬機器會先檢査老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼 Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視 HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次 Minor GC,儘管這次 Minor GC是有風險的;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次 Full GC.

為什麼要擔保:

Minor GC後還有大量物件存活且空間不夠存放新物件,就要直接在老年代存放

為什麼是歷次晉升到老年代物件的平均大小:

取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次 Minor GCd存活後的物件突增,遠遠高於平均值的話,依然會導致擔保失敗( HandlePromotionFailure)如果出現了 HandlePromotionFailure失敗,那就只好在失敗後重新發起一次 Full GC。雖然擔保失敗時繞的子是最大的,但大部分情況下都還是會將 HandlePromotionFailure開關開啟,避免 Full GC過於頻繁。

eden的大小範圍預設是 =【-XX:G1NewSizePercent,-XX:G1MaxNewSizePercent】=【整堆5%,整堆60%】

humongous如果一個物件的大小已經超過Region大小的50%了,那麼就會被放入大物件專門的Region中,這種Region我們叫humongous

G1垃圾回收流程



網上對G1的回收階段有不同的說法,參考Oracle JVM工程師的一個說法:

他把整個 G1 的垃圾回收階段分成了這麼三個,第一個叫 Minor GC,就是對新生代的垃圾收集,第二個階段呢叫 Minor GC + Concurrent Mark,就是新生代的垃圾收集同時呢會執行一些併發的標記,這是第二個階段,第三個階段呢它叫 Mixed GC 混合收集,這三個階段是一個迴圈的過程。剛開始是這個新生代的垃圾收集,經過一段時間,當老年代的記憶體超過一個閾值了,它會在新生代垃圾收集的同時進行併發的標記,等這個階段完成了以後,它會進行一個混合收集,混合收集就是會對新生代、倖存區還有老年代都來進行一個規模較大的一次收集,等記憶體釋放掉了,混合收集結束。這時候伊甸園的記憶體都被釋放掉,它會再次進入新生代的一個垃圾收集過程,那我們先來看看這個新生代的收集 Minor GC。

Minor GC的回收過程(eden滿了回收)

選定所有Eden Region放入CSet,使用多執行緒複製演算法將CSet的存活物件複製到Survivor Region或者晉升到Old Region。

下圖分7步演示了這個過程

1 初始狀態,堆無佔用

2 Eden Region滿了進行標記

3 將存活物件複製到Survivor Region

4 清理Eden Region

5 Eden Region又滿了進行再次標記,此時會連帶Survivor Region一起標記

6 將存活物件複製到另一個Survivor Region

7 再次清理Eden Region和被標記過的Survivor Region



Minor GC結束後自動進行併發標記,為以後可能的Mixed GC做準備

Mixed GC的回收過程(專注垃圾最多的分割槽)

選定所有Eden Region和全域性併發標記計算得到的收益較高的部分Old Region放入CSet,使用多執行緒複製演算法將CSet的存活物件複製到Survivor Region或者晉升到Old Region。

當堆空間的佔用率達到一定閾值後會觸發Mixed GC(預設45%,由引數決定)

Mixed GC它一定會回收年輕代,並會採集部分老年代的Region進行回收的,所以它是一個“混合”GC。

下圖分3步演示了這個過程

1 併發標記所有Region

2 併發複製

3 併發清理





ZGC

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

目標低延遲

保證最大停頓時間在幾毫秒之內,不管你堆多大或者存活的物件有多少。
可以處理 8MB-16TB 的堆

通過以上歷代垃圾回收器的講解,我們大致瞭解到減少延遲的底層思想不外乎將stop the world進行極限壓縮,將能並行的部分全部採用和使用者執行緒並行的方式處理,然而ZGC更"過分"它甚至把一分部垃圾回收的工作交給了使用者執行緒去做,那麼它是怎麼做到的呢?ZGC的標記和清理工作同CMS、G1大致差不多,仔細看下圖的過程,和CMS特別像,這就是我在上文說的CMS其實並沒有真正被拋棄,它的部分思想在ZGC 有發揚。



ZGC 的步驟大致可分為三大階段分別是標記、轉移、重定位。

標記:從根開始標記所有存活物件

轉移:選擇部分活躍物件轉移到新的記憶體空間上

重定位:因為物件地址變了,所以之前指向老物件的指標都要換到新物件地址上。

並且這三個階段都是併發的。

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

併發轉移準備 :分析最有回收價值GC分頁(無STW) 初始轉移應對初始標記的資料

併發轉移應對併發標記的資料

除了標記清理過程繼承了CMS和G1的思想,ZGC要做了以下優化

併發清理(轉移物件)

在 CMS 和 G1 中都用到了寫屏障,而 ZGC 用到了讀屏障。

寫屏障是在物件引用賦值時候的 AOP,而讀屏障是在讀取引用時的 AOP。

比如 Object a = obj.foo;,這個過程就會觸發讀屏障。

也正是用了讀屏障,ZGC 可以併發轉移物件,而 G1 用的是寫屏障,所以轉移物件時候只能 STW。

簡單的說就是 GC 執行緒轉移物件之後,應用執行緒讀取物件時,可以利用讀屏障通過指標上的標誌來判斷物件是否被轉移。

讀屏障會對應用程式的效能有一定影響,據測試,對效能的最高影響達到 4%,但提高了 GC 併發能力,降低了 STW。這就是上面所說的ZGC“過分”地將部分垃圾回收工作交給使用者執行緒的原因。

染色指標

染色指標其實就是從 64 位的指標中,拿幾位來標識物件此時的情況,分別表示 Marked0、Marked1、Remapped、Finalizable。



0-41 這 42 位就是正常的地址,所以說 ZGC 最大支援 4TB (理論上可以16TB)的記憶體,因為就 42 位用來表示地址

也因此 ZGC 不支援 32 位指標,也不支援指標壓縮。

其實物件只需要兩個狀態Marked,Remapped,物件被標記了,物件被重新映射了,為什麼會有M0,M1,用來區分本次GC標記和上次GC標記

以下是標記轉移演算法說明:

1 在垃圾回收開始前:Remapped

2 標記過程:

標記執行緒訪問

發現物件地址檢視是 Remapped 這時候將指標標記為 M0

發現物件地址檢視是 M0,則說明這個物件是標記開始之後新分配的或者已經標記過的物件,所以無需處理

應用執行緒

如果建立新物件,則將其地址檢視置為 M0

3 標記階段結束後

ZGC 會使用一個物件活躍表來儲存這些物件地址,此時活躍的物件地址檢視是 M0

4 併發轉移階段

轉移執行緒:

轉移成功後物件地址檢視被置為 Remapped(也就是說 GC 執行緒如果訪問到物件,此時物件地址檢視是 M0,並且存在或活躍表中,則將其轉移,並將地址檢視置為 Remapped )

如果在活躍表中,但是地址檢視已經是 Remapped 說明已經被轉移了,不做處理。

應用執行緒:

如果建立新物件,地址檢視會設為 Remapped

5 下次標記使用M1

M1 標識本次垃圾回收中活躍的物件

M0 是上一次回收被標記的物件,但是沒有被轉移,且在本次回收中也沒有被標記活躍的物件。

下圖展示了Marked,Remapped的過程,

初始化時A,B,C三個物件處於Remapped狀態

第一次GC,A被轉移,B未被轉移,C無引用將被回收

第二次GC,由於A被轉移過了(Remapped狀態),所以被標記M1,此時恰好B為不活躍物件,將被清理

第三次GC,A又被標記成M0



多重對映

Marked0、Marked1和Remapped三個檢視

ZGC為了能高效、靈活地管理記憶體,實現了兩級記憶體管理:虛擬記憶體和實體記憶體,並且實現了實體記憶體和虛擬記憶體的對映關係 在ZGC中這三個空間在同一時間點有且僅有一個空間有效,利用虛擬空間換時間,這三個空間的切換是由垃圾回收的不同階段觸發的,通過限定三個空間在同一時間點有且僅有一個空間有效高效的完成GC過程的併發操作



支援NUMA

NUMA是非一致記憶體訪問的縮寫 (Non-Uniform Memory Access,NUMA)

早年如下圖:SMP架構 (Symmetric Multi-Processor),因為任一個 CPU 對記憶體的訪問速度是一致的,不用考慮不同記憶體地址之間的差異,所以也稱一致記憶體訪問(Uniform Memory Access, UMA )。這個核心越加越多,漸漸的匯流排和北橋就成為瓶頸,那不能夠啊,於是就想了個辦法。





把 CPU 和記憶體整合到一個單元上,這個就是非一致記憶體訪問 (Non-Uniform Memory Access,NUMA)。



ZGC 對 NUMA 的支援是小分割槽分配時會優先從本地記憶體分配,如果本地記憶體不足則從遠端記憶體分配。

ZGC優劣

綜上分析,ZGC在戰略上沿用了上幾代GC的演算法策略,採用併發標記,併發清理的思路,在戰術上,通過染色指標、多重對映,讀屏障等優化達到更理想的併發清理,通過支援NUMA達到了更快的記憶體操作。但ZGC同樣不是銀彈,它也有自身的優缺點,如下

優勢:

1、一旦某個Region的存活物件被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正後才能清理,這使得理論上只要還有一個空閒Region,ZGC就能完成收集。

2、顏色指標可以大幅減少在垃圾收集過程中記憶體屏障的使用數量,ZGC只使用了讀屏障。

3、顏色指標具備強大的擴充套件性,它可以作為一種可擴充套件的儲存結構用來記錄更多與物件標記、重定位過程相關的資料,以便日後進一步提高效能。

劣勢:

1、它能承受的物件分配速率不會太高

ZGC準備要對一個很大的堆做一次完整的併發收集。在這段時間裡面,由於應用的物件分配速率很高,將創造大量的新物件,這些新物件很難進入當次收集的標記範圍,通常就只能全部當作存活物件來看待——儘管其中絕大部分物件都是朝生夕滅的,這就產生了大量的浮動垃圾。如果這種高速分配持續維持的話,每一次完整的併發收集週期都會很長,回收到的記憶體空間持續小於期間併發產生的浮動垃圾所佔的空間,堆中剩餘可騰挪的空間就越來越小了。目前唯一的辦法就是儘可能地增加堆容量大小,獲得更多喘息的時間。

2、吞吐量低於G1 GC

一般來說,可能會下降5%-15%。對於堆越小,這個效應越明顯,堆非常大的時候,比如100G,其他GC可能一次Major或Full GC要幾十秒以上,但是對於ZGC不需要那麼大暫停。這種細粒度的優化帶來的副作用就是,把很多環節其他GC裡的STW整體處理,拆碎了,放到了更大時間範圍內裡去跟業務執行緒併發執行,甚至會直接讓業務執行緒幫忙做一些GC的操作,從而降低了業務執行緒的處理能力。

總結

綜上,其實ZGC並不是一個憑空冒出的全新垃圾回收,它結合前幾代GC的思想,同時在戰術上做了優化以達到極限的STW,ZGC的優秀表現有可能會改變未來程式編寫方式,站在垃圾收集器的角度,垃圾收集器特別喜歡不可變物件,原有程式設計方式鑑於記憶體、GC能力所限使用可變物件來複用物件而不是銷燬重建,試想如果有了ZGC的強大回收能力的加持,是不是我們就可以無腦的使用不可變物件進行程式碼編寫

參考:

《深入理解java虛擬機器》

《JAVA效能權威指南》

JDK 發展至今的垃圾回收機制

全網最全JDK1~JDK15十一種JVM垃圾收集器的原理總結

為什麼CMS需要初始標記?

一步一圖帶你理清G1垃圾回收流程

美團面試官問我:ZGC 的 Z 是什麼意思?

ZGC有什麼缺點?

ZGC 原理是什麼,它為什麼能做到低延時?

本文件示意圖原型:https://www.processon.com/view/link/63771d355653bb3a840c4027