作者:京東科技 文濤
前言
本文所有介紹僅限於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 總時間的比例
通過以下兩種方式可達到目的:
- 在多 CPU 環境中使用多條 GC 線程,從而垃圾回收的時間減少,從而用户線程停頓的時間也減少;
- 實現 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