依然順滑!Dragonwell 11如何改造全新垃圾回收器ZGC? | 龍蜥技術

OpenAnolis小助手發表於2021-11-22

本文是 Alibaba Dragonwell ZGC 系列的第三篇技術分享,重點介紹我們在 Dragonwell 11 上對 ZGC 的生產就緒改造工作,從而有效應對了本系列第一篇提到的 OpenJDK 11 實驗性 ZGC 的風險。文末將小結 Dragonwell ZGC 系列文章,並展望未來發展方向。

相關閱讀:

絲般順滑!全新垃圾回收器 ZGC 初體驗 | 龍蜥技術

中篇|絲般順滑!全新垃圾回收器 ZGC 原理與調優


ZGC生產就緒改造

本系列第一篇文章 曾提到阿里巴巴將 Dragonwell11 上的 ZGC 改造為生產就緒版本,同時也提到 OpenJDK12-16 並非長期支援版本導致難以在生產中大規模部署。那麼問題來了,為何不用 OpenJDK 11 的 ZGC,而需要用Dragonwell 11 的呢?Dragonwell 11 的 ZGC 有什麼具體的適用場景?

我們認為,只要採用 Java 11,那麼您就應該選擇 Dragonwell 11 的生產就緒 ZGC,而不是 OpenJDK 11 的實驗性 ZGC。其中最重要的理由,就是 實驗性 ZGC 有機率發生無徵兆的崩潰現象 (參考本系列第一篇文章),而該問題在生產就緒 ZGC 中得以修復。Dragonwell 11 的 ZGC 還完善了許多功能,讓 ZGC 能夠解鎖更多的場景。

Dragonwell 11 的生產就緒 ZGC 有諸多優勢,包括:

  • ZGC 功能完善且能解決實踐中遇到的問題;
  • 保持 Java 11 長期支援的質量穩定性;
  • 完整的開源和測試流程。

ZGC 功能完善

Dragonwell 11 移植了 OpenJDK 15(首個支援生產就緒 ZGC 的 JDK 正式版本)的大部分 ZGC 相關程式碼,這些程式碼完善了 ZGC 的功能,支援更多的平臺,並且修復了讀屏障的重大 bug。

ZGC 重構 C2 讀屏障

本系列第一篇文章提到,我們發現 ZGC 讀屏障與載入操作的中間可能進入 GC 暫停。這是因為 ZGC 讀屏障採用 C2 即時編譯生成平臺相關程式碼,而 OpenJDK 11 的 ZGC C2 讀屏障可能產生上述的情形,由此引發錯誤。我們對照 OpenJDK 14 對於 ZGC C2 讀屏障的改造,取消讀屏障 C2 節點,對載入操作相關的 C2 節點進行改造,並使之在機器碼生成階段能夠生成正確的讀屏障和載入操作。
我們後續的實踐表明, C2 讀屏障重構可以消除 ZGC 崩潰現象 ,進一步提高了  ZGC 在生產實踐中的可用性。

ZGC 多平臺支援

Dragonwell 11 新增支援 AArch64/Linux。許多阿里業務和雲上客戶希望在 AArch64 平臺上也能用到 ZGC 的能力,同時也需要 OpenJDK 11 的長期支援。因此 Dragonwell 11 移植了 ZGC 與 AArch64 相關的生產就緒程式碼,從而拓寬了 ZGC 適用的機型。

ZGC 類解除安裝支援

類解除安裝是完整的 GC不 可或缺的一環,負責解除安裝 Java 中不再活躍的類。許多 Java 程式碼中會生成大量的類,而不再活躍的類和物件需要及時回收,否則將填滿類資訊的元空間,影響後續程式碼的執行。
OpenJDK 對於 ZGC 的併發類解除安裝功能是在 OpenJDK 12 中完成的。 這個過程伴隨著大量的公共資料結構(非 ZGC 程式碼)的併發化改造。這些大量公共資料結構改造包含上百個程式碼改動, 稍有不慎就會導致難以控制的程式碼風險 ,而且後續與上游同步成本會隨之增高。
Dragonwell 11 參考了現有 GC 的類解除安裝程式碼,結合 OpenJDK 12中 ZGC 關於類解除安裝功能的程式碼,實現了 ZGC 的類解除安裝功能。儘管 ZGC 的類解除安裝功能不是併發的, 目前我們的實踐顯示這一類解除安裝過程能將暫停保持在 10ms 級別 。由於類解除安裝在許多業務中不是頻繁發生的,因此我們讓 Dragonwell 11 支援 ZUnloadClassesFrequency 選項來調節類解除安裝的頻率。

ZGC 記憶體使用最佳化

Dragonwell 11 ZGC 相比於 OpenJDK 11 增加了歸還實體記憶體、記憶體規格的擴充套件、並行 pre-touching 的支援。這些新增的支援能夠幫助 Dragonwell 11  ZGC 解鎖更多細化的場景。

歸還實體記憶體: ZGC 的歸還實體記憶體功能適用於“同一個機器部署多個例項”的場景。Dragonwell 11 的 ZGC 可透過設定 ZUncommit 來開啟歸還實體記憶體的功能。開發者只要設定堆的上限 Xmx、下限 Xms 以及 SoftMaxHeapSize,Java 業務平時使用的堆大小將保持在 Soft Max Heap Size 左右。當有突發流量到來之時,Java 業務可以臨時擴大堆的大小,以應對突發流量;當突發流量過去了以後,還可以將暫時用不到的記憶體歸還給作業系統。

記憶體規格的擴充套件: Dragonwell 11 擴充套件了 ZGC 的適用記憶體規格,能夠支援 16TB 的超大堆和 8MB 的超小堆,使得同一個業務部署不同規格的機器更加方便。

並行pre-touching: GC 的 pre-touching 的能力(開啟-XX:+AlwaysPreTouch)可以讓業務的 RT 免遭業務剛啟動時記憶體 touch 的影響,而 JDK 11 中的 ZGC pre-touching 是單執行緒的,導致應用啟動時候需要消耗很長的時間(大堆的 pre-touching 過程可達到分鐘級別)。Dragonwell 11 並行化改造了 pre-touching 的過程,使得大堆業務的啟動速度得以提升。

ZGC 響應時間最佳化

這裡的響應時間是關於 非暫停因素 影響 RT P99/P999 的情況。

在 JDK 11 的 ZGC 實踐過程當中,我們可能會看到 Page Cache Flush。

    [2019-09-05T14:14:04.242+0800] GC(10816) Page Cache Flushed: 28M requested, 28M(11424M->11396M) flushed
    [2019-09-05T14:14:04.248+0800] Page Cache Flushed: 32M requested, 32M(11928M->11896M) flushed
    [2019-09-05T14:14:04.259+0800] Page Cache Flushed: 32M requested, 32M(11912M->11880M) flushed
    [2019-09-05T14:14:04.271+0800] Page Cache Flushed: 32M requested, 32M(11878M->11846M) flushed
    [2019-09-05T14:14:04.276+0800] Page Cache Flushed: 32M requested, 32M(11846M->11814M) flushed
    ... (省略35個"Page Cache Flushed")
    [2019-09-05T14:14:04.462+0800] Page Cache Flushed: 32M requested, 32M(10596M->10564M) flushed
    [2019-09-05T14:14:04.467+0800] Page Cache Flushed: 32M requested, 32M(10564M->10532M) flushed
    [2019-09-05T14:14:04.471+0800] Page Cache Flushed: 32M requested, 32M(10522M->10490M) flushed
    [2019-09-05T14:14:04.477+0800] GC(10816) Page Cache Flushed: 32M requested, 32M(10490M->10458M) flushed
    我們同時會在業務的監控上看到 RT P99 升高到 200ms 以上。如上圖,因為發生了連續多次 Page Cache Flushed,持續時間長達 200ms 以上。此時 Page Cache Flush 引發了執行緒阻塞,幾十個物件分配執行緒均等候在同一個鎖上。

    這是因為 ZGC 把堆劃分成若干個 ZPage (與 G1 的 Region 概念相同), 包括小型 (2MB), 中型 (32MB), 大型 (2*N MB)三種規格 。物件分配時會把物件按照大小分配到相應規格的 ZPage 當中。Page Cache 是存放空閒 ZPage 的資料結構。

    我們在實際執行當中遇到一個問題,即不同規格物件分配速率不穩定。有時候中型物件更多,那麼就會導致中型  ZPage 變少,需要把小型/大型 ZPage 轉化成中性 ZPage。這個轉化動作就是 Page Cache Flush。Page Cache Flush 耗時較長,需要多次進行 mmap 系統呼叫(開銷較大);Page Cache Flush 影響面大,需要鎖住 ZPage 分配全域性鎖。

    Dragonwell 11 的解決辦法是 移植“提升 ZPage 分配併發度”的特性 。這個特性可以儘可能避免使用 ZPage 分配全域性鎖,並且非同步執行 mmap。Dragonwell 11 的另一個解決辦法是 調整中型 ZPage 的物件大小閾值 (原來的範圍:256KB~4MB),我們新增支援設定  ZMediumObjectUpperBound ,例如-XX:ZMediumObjectUpperBound=10MB  (代表調整後中等 ZPage 的範圍:256KB~10MB)。實踐表明,Dragonwell 11 可以大幅減少了 Page Cache Flush 引發的執行緒阻塞,從而最佳化 RT P99/P999。

    ZGC 吞吐率問題處理

    ZGC 在生產實踐中有機率遇到吞吐率不足的情形,包括兩種現象:分配暫停 Allocation Stall 和記憶體不足 OOM(Out of Memory)。

    現象1:  分配暫停 Allocation Stall (回收速度跟不上分配速度)

    開發人員增加堆大小(Xmx)或併發 GC 執行緒數量( ConcGCThreads )可以緩解這一現象。然而機器的計算資源是有限的,不可能無限制地增加堆和執行緒數。這時候就要 考慮 ZGC 的觸發時機:

    (1) ZAllocationSpikeTolerance: 這是 ZGC 在 JDK11 中就已經支援的,增加該引數可以處理分配速率毛刺,但是增加該引數不適應日常情形,過度觸發 ZGC 導致 CPU 消耗過高;

    (2) ZHighUsagePercent: 一些業務對接的線上監控在堆的水位過高時候會報警。Experimental ZGC 對 ZGC 水位並沒有絕對的限制。Product ready ZGC設定了 95% 作為堆的最高水位。Dragonwell 11 可以透過 ZHighUsagePercent 調節堆最高水位,當堆水位超過ZHighUsagePercent%時觸發ZGC。

    現象2: 記憶體 不足 OOM

    ZGC 預留了固定的空間作為物件轉移的區域,但是如果Java執行緒訪問物件速度過快,就可能導致物件轉移速度過快,預留空間依然不足,最終導致 OOM,程式崩潰。

    Dragonwell 11 可以調節引數  ZRelocationReservePercent ,讓堆的 ZRelocationReservePercent% 作為預留空間,更大程度避免了 OOM 的情形。

    ZGC 監控升級

    Dragonwell 11 更新了 GC 日誌的細節:包括錯誤活躍物件資訊更正,並顯示不同規格 ZPage 的統計資訊。

    Dragonwell 11 還引入了 ZGC 相關的 JFR 事件:ZAllocationStall,ZPageAllocation,ZRelocationSet,ZRelocationSetGroup,ZUncommit,ZUnmap。這些 JFR 事件可以監控當前 ZGC 的狀況,有助於排查 ZGC 出現的異常狀況。同時更新了 ZGC 相關的 GarbageCollectorMXBean,從而可以監控 ZGC 的兩種指標:ZGC 週期和 ZGC 暫停。

    保持質量穩定性

    阿里巴巴 Dragonwell 11 有選擇地移植生產就緒 ZGC 程式碼,並且對這部分程式碼 進行合理地改造 ,使得 Dragonwell 11 既擁有 OpenJDK15 的 ZGC 能力,也能夠享受到 OpenJDK11  長期支援的質量穩定性

    我們注意到,如果不加控制地移植所有 ZGC 程式碼,則有可能修改Dragonwell 11 的公共部分的大量程式碼。這樣帶來的後果包括:

    • 後續升級困難:Dragonwell 11 會定期同步上游最新的 OpenJDK11 的程式碼,如果 OpenJDK11 的更新與我們的 Dragonwell 11 ZGC 改造同時修改了這部分程式碼,那麼這部分程式碼將難以維護,增加程式碼出錯的風險。

    • 影響 Dragonwell 11 的其他部分程式碼的正確性:ZGC 依賴的公共程式碼改動,包括一些類載入和 C2 公共程式碼的改動。其他的 GC(包括 G1/CMS等)乃至 JDK 的其餘部分事實上也呼叫了這部分程式碼。如果沒有仔細移植公共程式碼改動,確認這些改動不會影響正確性,那麼使用者可能遭遇意想不到的風險。

    因此我們需要對程式碼風險進行控制,把生產就緒改造儘可能控制在 ZGC 程式碼的範圍之內,選擇與生產就緒最相關的 ZGC 程式碼進行合理改造。

    為了把改動控制在 ZGC 程式碼範圍之內,我們採用了編譯時檢查和執行時檢查的方式,保證 ZGC 改造程式碼不會“汙染”公共程式碼。(這部分工作參考了 Shenandoah GC Backport to JDK11 的工作)

    編譯時檢查採用“宏隔離”的方式 ,在關閉 ZGC 編譯時,程式碼不會被編譯,從而確保程式碼沒有問題。這樣的做法可以保證 ZGC 開啟編譯時的程式碼質量。“宏隔離”即採用宏的方式進行隔離:

      #if INCLUDE_… #endif
      ZGC_ONLY( … )
      執行時檢查採用“條件隔離”的方式 ,保證其他 GC 開啟時候不會執行到我們移植的程式碼,進一步降低 Dragonwell 11 的風險。“條件隔離”即採用 if 語句進行隔離:
        if (UseZGC) { … }

        開源與測試流程

        我們在 GitHub 開源了 ZGC 生產就緒改造的過程,記錄在了里程碑 ()中。里程碑囊括了兩百餘個 ZGC 相關 patch。每個 patch 都得到了阿里巴巴專家的精心 review。

        我們維護了負責測試的 Nightly build 流水線,保證每晚都能夠在 x64 和 AArch64 平臺上正常編譯,並且開啟/關閉 ZGC 都能透過 OpenJDK 的測試。


        展望

        我們注意到 ZGC 的一些最新進展,可以進一步最佳化 ZGC 的效能,包括:

        1、類指標壓縮 (compressed class pointers)。我們的內部實驗顯示,由於類指標壓縮,ZGC 效能得到提升明顯(儘管物件指標沒有壓縮)。由於程式碼移植對 JDK11 的穩定性有影響,因此暫未開源。
        2、原地物件轉移: JDK16 ZGC 採用了原地物件轉移的技術,避免 OOM 的發生。
        3、亞毫秒級別暫停: JDK16 ZGC 支援了併發執行緒棧處理,從而把 GC Roots的處理也放在併發執行緒中處理,達到 1ms 以內的暫停時間。
        4、吞吐率提升: 近期 ZGC 在自己的程式碼庫中公開了分代 ZGC 的程式碼,有望提升 ZGC 的吞吐率。
        我們還評估了 ZGC 的姊妹:Shenandoah GC。我們初步評估發現,Shenandoah GC 在 32GB 以內的堆上效果較好,其中最重要的因素是它對於指標壓縮的支援。

        小結

        Alibaba Dragonwell ZGC 系列從 GC 概念,談到 ZGC 及其適用場景,以及Dragonwell 11 對 ZGC 的生產就緒改造。這項工作維持了 Dragonwell 11 的穩定性,同時把 ZGC 升級到了生產就緒的 ZGC:修復了 ZGC 重大缺陷,新增支援 AArch64 平臺,以及眾多新功能的完善。Dragonwell 11 還新增了若干通用特性,適應阿里內部和雲上客戶的需求。

        未來我們還將不定期更新 Alibaba Dragonwell ZGC 系列,分享我們使用 ZGC 的經驗,以及我們在 OpenJDK 的相關貢獻。

        相關連結

        Alibaba_Dragonwell_11.0.11.7:

        ZGC 專案主頁:

        ZGC 官方介紹:

        ~pliden/slides/ZGC-OracleDevLive-2020.pdf

        關於作者
        唐浩,2019 年加入阿里雲程式語言與編譯器團隊,目前從事 JVM 記憶體管理最佳化方向的工作。
        歡迎加入SIG

         DragonWell 已加入 龍蜥社群 (OpenAnolis )Java 語言與虛擬機器 SIG,同時龍蜥作業系統(Anolis OS )8 版 本支援 DragonWell 雲原生 Java ,歡迎大家加入社群 SIG,參與社群共建。

        SIG 地址

        官網:

        —— 完 ——
        加入龍蜥社群

        加入微信群:新增社群助理-龍蜥社群小龍(微信:openanolis_assis),備註【龍蜥】拉你入群;加入釘釘群:掃描下方釘釘群二維碼。歡迎開發者/使用者加入龍蜥社群(OpenAnolis)交流,共同推進龍蜥社群的發展,一起打造一個活躍的、健康的開源作業系統生態!

        關於龍蜥社群

        龍蜥社群(OpenAnolis) 是由 企事業單位、高等院校、科研單位、非營利性組織、個人等按照自願、平等、開源、協作的基礎上組成的非盈利性開源社群。龍蜥社群成立於2020年9月,旨在構建一個開源、中立、開放的Linux上游發行版社群及創新平臺。

        短期目標是開發龍蜥作業系統Anolis OS作為CentOS替代版,重新構建一個相容國際Linux主流廠商發行版。中長期目標是探索打造一個面向未來的作業系統,建立統一的開源作業系統生態,孵化創新開源專案,繁榮開源生態。

        龍蜥OS 8.4已釋出,支援x86_64和ARM64架構,完善適配Intel、飛騰、海光、兆芯、鯤鵬晶片。

        歡迎下載:

        加入我們,一起打造面向未來的開源作業系統!


        來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70004278/viewspace-2843392/,如需轉載,請註明出處,否則將追究法律責任。

        相關文章