
我們在上一篇文章中介紹了 ZGC 的基本概念和阿裡的 ZGC 規模化實踐,看到阿裡業務和雲上客戶享受到 ZGC 帶來的響應時間優化,同時也遭遇到了一些實際問題。為了更好地使用 ZGC,我們需要了解一些 ZGC 的原理,并學會分析 ZGC 日志,對 ZGC 進行調優。
相關閱讀《上篇|絲般順滑!全新垃圾回收器 ZGC 初體驗》ZGC原理
從宏觀的角度看,ZGC 是一種并發( concurrent )的壓縮式( compacting ) GC 算法:
- 并發:在 Java 線程運作的同時,GC 線程在背景默默執行;
- 壓縮式:定期将堆中活躍對象整理到一起,解決記憶體碎片化問題。
相比于 Java 原有的百毫秒級的暫停的 Parallel GC 和 G1,以及未解決碎片化問題的 CMS ,并發和壓縮式的 ZGC 可謂是 Java GC 能力的一次重大飛躍—— GC 線程在整理記憶體的同時,可以讓 Java 線程繼續執行。
ZGC 采用标記-壓縮政策來回收 Java 堆:ZGC 首先會并發标記( concurrent mark )堆中的活躍對象,然後并發轉移( concurrent relocate )将部分區域的活躍對象整理到一起。這裡與早先的 Java GC 不同之處在于,目前 ZGC 是單代垃圾回收器,在标記階段會周遊堆中的全部對象。
那麼問題來了,ZGC 是如何做到并發标記和轉移呢?這就要提到ZGC背後的核心技術——讀屏障( load barrier )和染色指針( colored pointer )。
ZGC 的讀屏障是在指針加載的操作的時候,插入一段針對該指針的處理邏輯:
- 如果指針指向已經被轉移的對象,那麼讀屏障将修正該指針;
- 在标記階段,如果該指針未被标記,那麼讀屏障将标記該指針;
- 在轉移階段,如果該指針指向需要轉移的區域,那麼該指針指向的對象将被轉移,然後修正該指針。
讀屏障能夠確定在 GC 線程與 Java 線程并發運作的情況下,每次指針載入都能通路到正确的對象。
ZGC 的染色指針是将指針高位未使用的 bit 作為指針的顔色,表示指針的狀态,使得讀屏障在處理指針的時候,能夠直接得到該指針的狀态,決定采用何種方式來處理指針。生産就緒的 ZGC 支援 2^44=16TB 的尋址空間,實際上使用了 44+4=48 個 bit 作為染色指針的位址,其中高 4 位是指針的顔色。染色指針與讀屏障互相配合,将讀屏障中的條件判斷部分轉換為對于指針顔色的判斷,如果指針顔色是“錯誤”的,那麼讀屏障就會将指針修複為“正确”的。
ZGC日志分析
單次ZGC周期實際執行過程中需要三次短促的暫停,每次暫停之後是若幹并發階段。
[2020-12-23T13:30:57.402+0800] GC(10) Garbage Collection (Allocation Rate)
[2020-12-23T13:30:57.408+0800] GC(10) Pause Mark Start 2.918ms
[2020-12-23T13:30:58.083+0800] GC(10) Concurrent Mark 674.216ms
[2020-12-23T13:30:58.087+0800] GC(10) Pause Mark End 1.336ms
[2020-12-23T13:30:58.105+0800] GC(10) Concurrent Process Non-Strong References 18.293ms
[2020-12-23T13:30:58.111+0800] GC(10) Concurrent Reset Relocation Set 5.533ms
[2020-12-23T13:30:58.111+0800] GC(10) Concurrent Destroy Detached Pages 0.001ms
[2020-12-23T13:30:58.121+0800] GC(10) Concurrent Select Relocation Set 10.148ms
[2020-12-23T13:30:58.130+0800] GC(10) Concurrent Prepare Relocation Set 9.083ms
[2020-12-23T13:30:58.136+0800] GC(10) Pause Relocate Start 2.452ms
[2020-12-23T13:30:58.203+0800] GC(10) Concurrent Relocate 66.595ms
... (此處忽略一些資料統計資訊)
[2020-12-23T13:30:58.203+0800] GC(10) Garbage Collection (Allocation Rate) 62020M(76%)->41270M(50%)
上面的 GC 日志展示了一個典型的 ZGC 周期,每一行每個周期中以 Pause 開頭的階段即為暫停階段,這三個暫停階段分别為
- Pause Mark Start(标記開始暫停);
- Pause Mark End(标記結束暫停);
- Pause Relocate Start(轉移開始暫停)。
可以看到上面的 GC 日志中,ZGC 三個暫停階段的時間明顯低于 10ms 。這三個暫停階段主要承擔 GC Roots 的标記和轉移,以及标記線程同步的工作。
這三個暫停階段後面以 Concurrent 開頭的階段即為并發階段,其中最核心的兩個階段是
- Concurrent Mark(并發标記);
- Concurrent Relocate(并發轉移)。
其餘的并發階段主要是并發轉移之前的一些預備工作。
ZGC 各個階段的圖示
目前 ZGC 的并發标記會标記整個堆中的所有活躍對象,有别于 G1/CMS/Parallel GC 等分代 GC ,屬于單代 GC 。并發标記的過程會順便修複堆中的錯誤指針。為了降低轉移對象的負擔,ZGC 的并發轉移的政策會選擇碎片化程度達到某個門檻值(ZFragmentationLimit)的區域,類似于 G1 的 Garbage First 政策。
ZGC調優
下面介紹ZGC相關的調優細節,使用者應至少完成基本調優部分。
基本調優
一般來說,ZGC 應當設定堆空間大小( Xmx )和并發 GC 線程數量(ConcGCThreads)。我們建議所有 ZGC 使用者應當開啟 GC 日志,通常建議開啟- Xlog:gc*:gc.log:time ,能夠記錄較多的 ZGC 細節。
堆空間大小
GC 通常需要開發者指定堆空間大小,具體數值應該大于堆内活躍對象的總大小。備援空間比例越高,GC 性能通常越好。例如估計對象總大小達到 32GB ,可設定- Xmx40g ,代表開啟 40GB 的堆。
ZGC 與傳統 GC 的不同之處在于,ZGC 在回收對象的同時,Java 線程也在配置設定新對象。是以 ZGC 比傳統 GC 需要更高比例的備援空間。
每一輪 ZGC 的過程中配置設定的對象總大小可以用“配置設定速度·單輪 ZGC 時間”來估算,是以堆空間的大小應大于“活躍對象的總大小+單次 ZGC 期間配置設定的對象總大小”。
上述的“配置設定速度”和“單輪 ZGC 時間”均可以在 GC 日志中找到統計資訊。
并發GC線程數量
ZGC 預設的并發 GC 線程數量是 1/8 的 CPU 核數,例如 16 核的機器,如果沒有指定 ConcGCThreads,那麼 ZGC 就會開啟 2 個并發 GC 線程。
在 GC 日志中,如果頻繁出現“ Allocation Stall ”,代表回收跟不上配置設定的情況,那麼可能需要提高 ConcGCThreads 了。當然 ConcGCThreads 不能無限制地增加,因為過多并發的 GC 線程會占據 CPU 資源,甚至影響 Java 線程的正常執行。
注意并發 GC 線程(ConcGCThreads)與并行 GC 線程(ParallelGCThreads)是不同的,前者與 Java 線程可以并發執行,後者是 GC 暫停時的 GC 線程。
進階調優
Product ready ZGC 的功能同時也支援若幹進階 ZGC 調優選項,可參考 Alibaba Dragonwell 11.0.11.7 的使用說明
https://github.com/alibaba/dragonwell11/wiki/Alibaba-Dragonwell-11-Release-Notes#110117進階調優最核心的部分是 GC 觸發時機的控制。由于 ZGC 在回收時依然配置設定對象,于是 ZGC 不能等到堆空間滿了以後才觸發 GC ,而需要提前一段時間觸發 GC ,使得 ZGC 執行的過程中堆空間不會變滿,導緻 Allocation Stall 或者 OOM。但是如果 ZGC 觸發地過于頻繁,則 CPU 資源消耗變多,進而降低吞吐率。
Dragonwell11 支援以下 GC 觸發時機相關選項:
- ZAllocationSpikeTolerance:ZGC 通過估算“配置設定速度·單輪 ZGC 時間”來估算單次 ZGC 期間配置設定的對象總大小,隻要這個總大小小于目前剩餘的堆空間,就需要觸發 GC 。但是由于 Java 業務的配置設定速度往往不是穩定的,是以需要為配置設定速度乘上“毛刺系數” ZAllocationSpikeTolerance ,進而保守地提前觸發 GC 。如果 Java 業務配置設定速度不穩定,偶爾有 Allocation Stall 的發生,那麼就應當考慮适當增加 ZAllocationSpikeTolerance。
- ZCollectionInterval:定時觸發 GC,避免 GC 間隔過長。
- ZProactive:字面意思是“主動觸發 GC ”,用于處理配置設定速率較低的情況。
- ZHighUsagePercent:堆的水位超過此百分比,則觸發 ZGC 。
隻要滿足以上 GC 觸發時機的條件之一,ZGC 就會觸發。
SoftMaxHeapSize 選項可以設定 ZGC 堆空間的“軟上限”,介于 Xmx 和 Xms 之間。以上的 ZAllocationSpikeTolerance/ZProactive/ZHighUsagePercent 均以 SoftMaxHeapSize 的值作為 ZGC 堆空間的“軟上限”,當配置設定速度過快時可以擴充到至多 Xmx 的堆空間,當配置設定速度較慢時可以将堆空間收縮到 Xms 。SoftMaxHeapSize 通常需要打開 - XX:+ZUncommit 。
除此之外還有一些有用的進階調優功能:
- ZFragmentationLimit:控制 ZGC 的對象的碎片化程度,ZFragmentationLimit 越低,ZGC 回收越徹底;
- ZMarkStackSpaceLimit:調節 ZGC 标記棧空間大小;
- ZUnloadClassesFrequency:控制 ZGC 類解除安裝的頻率;
- ZRelocationReservePercent:控制 ZGC 的預留配置設定空間,降低 OOM 風險;
- ZStatisticsInterval:控制 ZGC 日志中統計資訊的輸出頻率,原先 10 秒一次輸出可能會影響 GC 詳細資訊的解讀。
(上面的 ZHighUsagePercent/ZUnloadClassesFrequency/ZRelocationReservePercent 是 Dragonwell11 的特有選項。若切換到其他版本的 OpenJDK 時,請避免使用這些選項。)
在後面的篇章中,讀者将看到我們的 Alibaba Dragonwell11 通過對 ZGC 進行生産就緒改造,進而解決生産實踐中的一些問題。
關于作者
唐浩,2019 年加入阿裡雲程式設計語言與編譯器團隊,目前從事 JVM 記憶體管理優化方向的工作。
現 DragonWell 已加入 龍蜥社群 (OpenAnolis )Java 語言與虛拟機 SIG,同時龍蜥作業系統(Anolis OS )8 版本支援 DragonWell 雲原生 Java ,歡迎大家加入社群 SIG,參與社群共建。
SIG 位址:
https://openanolis.cn/sig/java/doc/216166872482840581——完——
加入龍蜥社群
加入微信群:添加社群助理-龍蜥社群小龍(微信:openanolis_assis),備注【龍蜥】拉你入群;加入釘釘群:掃描下方釘釘群二維碼。歡迎開發者/使用者加入龍蜥OpenAnolis社群交流,共同推進龍蜥社群的發展,一起打造一個活躍的、健康的開源作業系統生态!
Dragonwell釘釘交流群 龍蜥社群釘釘交流群
關于龍蜥社群
龍蜥社群(OpenAnolis)是由企事業機關、高等院校、科研機關、非營利性組織、個人等按照自願、平等、開源、協作的基礎上組成的非盈利性開源社群。龍蜥社群成立于2020年9月,旨在建構一個開源、中立、開放的Linux上遊發行版社群及創新平台。
短期目标是開發龍蜥作業系統(Anolis OS)作為CentOS替代版,重新建構一個相容國際Linux主流廠商發行版。中長期目标是探索打造一個面向未來的作業系統,建立統一的開源作業系統生态,孵化創新開源項目,繁榮開源生态。
龍蜥OS 8.4已釋出,支援x86_64和ARM64架構,完善适配Intel、飛騰、海光、兆芯、鲲鵬晶片。歡迎下載下傳:
https://openanolis.cn/download加入我們,一起打造面向未來的開源作業系統!
Https://openanolis.cn