作者:京東科技 文濤
前言
本文所有介紹僅限於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 效能權威指南》
全網最全 JDK1~JDK15 十一種 JVM 垃圾收集器的原理總結
本文件示意圖原型:https://www.processon.com/view/link/63771d355653bb3a840c4027