低延遲垃圾收集器:挑戰“不可能三角”
----------- 先贊后看 ?? 效果翻倍 ?? ----------------
在開始之前,必須再次強調 “不可能三角”:內存占用、吞吐量、延遲,三者難以同時完美。
傳統的垃圾收集器(如 Serial, Parallel, CMS, G1)在堆內存變大時,停頓時間(Latency)也會顯著變長,因為它們總有一些階段需要“Stop The World”(掛起所有用戶線程)來完成清理工作。這對于需要快速響應的應用(如實時交易、大數據平臺、微服務)是無法接受的。
Shenandoah 和 ZGC 的目標就是打破這個魔咒,實現在任何堆大小下(比如 4TB),停頓時間都能被嚴格控制在十毫秒以內,同時盡可能減少對吞吐量的影響。
它們實現這一目標的核心理念是:將最耗時的“對象移動”階段也并發化。這聽起來簡單,但實現起來極其復雜,因為在你移動對象的同時,用戶線程可能正在讀寫它。兩者解決方案不同,但都極其精妙。
Shenandoah: “搬家隊長”與“轉發牌”
你可以把 Shenandoah 想象成一個高效的搬家隊長,它的核心絕招是 “轉發指針”(Brooks Pointer)。
1. 它是誰?
- 由 RedHat 開發,是 OpenJDK 的“養子”,OracleJDK 中不包含它。
- 你可以把它看作是 G1 的激進并發版,架構和 G1 很像(基于 Region 的堆布局),共享部分代碼。
2. 它如何實現并發搬家?(核心原理)
想象一下,你要給一個正在營業的超市換貨架,顧客還在不停買東西。你怎么做?
Shenandoah 的解決方案是:給每個商品(對象)掛上一個“轉發牌”。
-
準備工作:在每個對象內部,額外開辟一個小空間(在對象頭前),里面存著一個地址。正常情況下,這個地址指向對象自己,就像商品上掛著一個寫著“我在這里”的牌子。
-
開始搬家(并發回收階段):Shenandoah 啟動一個并發線程,悄悄地把需要回收的區域(Region)里的存活對象復制到新區域。這整個過程是不停止營業(用戶線程)的。
-
更新地址牌:對象復制完成后,Shenandoah 只做一件事:修改舊對象上的那個“轉發牌”,把上面的地址從“舊對象”改為“新對象”的地址。注意:它不會去更新所有指向這個舊對象的引用。
-
顧客訪問(讀/寫屏障):這就是關鍵所在!Shenandoah 在 JVM 中設置了“哨兵”(讀屏障和寫屏障)。每當用戶線程(顧客)要訪問一個對象時,哨兵會先攔截這次訪問,檢查一下這個對象上的“轉發牌”。
- 如果牌子指向自己:說明沒搬過家,直接訪問。
- 如果牌子指向別處:說明這個對象是舊對象,已經搬走了。哨兵會自動地、悄悄地根據牌子上的新地址,去訪問新對象,并把這次訪問的結果返回給用戶。同時,它還會順手把這個引用本身的值更新成新地址(只有第一次需要轉發,后續就直接訪問新對象了)。這個過程對用戶線程是完全透明的。
為什么能控制停頓?
因為最耗時的“復制對象”和“更新引用”工作都被并發線程和“哨兵”(屏障)分擔了。那些必需的短暫停頓(初始標記、最終標記等)只處理少量核心信息(GC Roots),與堆大小無關,所以非常短。
3. 優缺點
- 優點:停頓時間極短,與堆大小脫鉤。
- 缺點:“哨兵”(尤其是讀屏障)帶來的開銷非常大。因為每一次對象讀取操作都要經過這個檢查步驟。這導致 Shenandoah 的吞吐量損失通常是三者中最大的。
ZGC: “魔法指針”與“自愈能力”
ZGC 的思路更加科幻。它不像 Shenandoah 那樣給對象掛牌子,而是直接給指針(內存地址)施了魔法,它的核心是 染色指針(Colored Pointer)。
1. 它是誰?
- 由 Oracle 親兒子開發,血統純正,OpenJDK 和 OracleJDK 都包含。
- 它的設計理念源自傳說中的 Azul C4 收集器,非常前沿。
2. 它如何實現并發搬家?(核心原理)
繼續用超市搬家的比喻。ZGC 的做法不是掛牌子,而是它有一種“魔法墨水”,可以直接在寫有商品位置的導購圖(指針)上做標記。
-
魔法墨水:在 64 位系統中,我們其實用不了那么大的地址空間。ZGC 巧妙地利用了指針中未使用的比特位(比如高 4 位)來存儲信息。這些信息包括:對象是否被標記、是否屬于待回收集合、是否已被移動過。
所以,ZGC 的指針不僅僅是地址,它本身就是攜帶元數據的。通過這個指針,ZGC 不用訪問對象就能知道它的狀態。 -
地址重映射(魔法地圖):光在指針上寫墨水,CPU 可不認賬,它會把這些位也當成地址的一部分,會找錯地方。ZGC 的解決方案是 多重映射:它通過操作系統的內存管理功能,將好幾塊不同的虛擬內存地址空間(比如,指針標志位是 0010 的地圖和 0000 的地圖)都映射到同一塊真實的物理內存上。這樣,無論指針上的“魔法墨水”怎么寫,最終都能通過這張“魔法地圖”找到正確的物理對象。
-
并發搬家與自愈:當 ZGC 要移動一個對象時:
- 它并發地將對象復制到新 Region。
- 它在舊對象的位置上留下一個“轉發地址”(類似于轉發表)。
- 最關鍵的一步來了:當用戶線程試圖訪問一個已經被移動的舊對象時,ZGC 的“哨兵”(讀屏障)會被觸發。這個哨兵一看指針上的“魔法墨水”(標志位),就知道“哦,這個對象搬走了”。于是它:
- 去舊位置上的“轉發地址”里找到新地址。
- 直接把這個線程手中的指針值更新成新地址(并修正標志位)!
- 再去新地址訪問對象。
這個過程被稱為 “自愈”(Self-Healing)。這次訪問之后,這個引用本身就已經被修正了,下次再訪問就是直接訪問新對象,沒有任何額外開銷。 這是它與 Shenandoah 每次訪問都可能需要檢查的關鍵區別。
3. 優缺點
- 優點:
- 停頓時間極短,同樣與堆大小無關。
- “自愈”能力使得運行時開銷遠小于 Shenandoah,因此吞吐量表現通常比 Shenandoah 好得多,甚至接近 G1。
- 無需像 G1 那樣維護記憶集,節省了內存。
- 缺點:
- 實現極其復雜,嚴重依賴底層操作系統特性(如多重映射)。
- 不支持分代收集(目前),可能導致浮動垃圾較多,抗突發流量能力稍弱。不過這是工程選擇,并非技術不能實現。
總結與對比:你該選誰?
| 特性 | Shenandoah | ZGC |
|---|---|---|
| 核心原理 | 轉發指針 (Brooks Pointer) | 染色指針 (Colored Pointer) |
| 實現方式 | 在對象頭前加額外指針 | 利用指針的未使用位存儲元數據 |
| 關鍵機制 | 讀屏障 + 寫屏障 | 讀屏障(目前無需寫屏障) |
| 引用更新 | 每次訪問都可能需要轉發檢查 | “自愈”,僅第一次訪問慢 |
| 性能特點 | 低延遲,但吞吐量損失較大 | 低延遲,吞吐量損失很小 |
| 血緣關系 | 像 G1 的并發升級版 | 像 Azul C4 的 OpenJDK 實現 |
| 支持現狀 | 僅 OpenJDK 包含 | OpenJDK 和 OracleJDK 都包含 |
如何選擇?
- 追求極致低延遲,且運行在 OpenJDK 上:兩者都是絕佳選擇。
- 同時非常關心吞吐量性能:優先嘗試 ZGC,它的性能表現通常更均衡。
- 需要使用 OracleJDK 并獲得商業支持:只能選擇 ZGC。
- 應用分配速率極高,堆內存巨大:目前兩者都可能因為不分代而面臨浮動垃圾的壓力,需要預留足夠堆內存。這是所有不分代收集器的共性問題。
總而言之,Shenandoah 和 ZGC 都代表了 JVM 垃圾收集技術的最高水平,它們通過不同的魔法將延遲降低到了前人無法想象的程度。ZGC 憑借其“魔法指針”和“自愈”能力,在實現上更顯優雅,性能開銷也更小,是目前更受矚目的未來之星。
?? 如果你喜歡這篇文章,請點贊支持! ?? 同時歡迎關注我的博客,獲取更多精彩內容!
本文來自博客園,作者:佛祖讓我來巡山,轉載請注明原文鏈接:http://www.rzrgm.cn/sun-10387834/p/19095137

浙公網安備 33010602011771號