溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

發(fā)布時(shí)間:2021-12-27 15:30:01 來(lái)源:億速云 閱讀:156 作者:柒染 欄目:大數(shù)據(jù)

今天就跟大家聊聊有關(guān)怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延遲垃圾回收器,它的設(shè)計(jì)目標(biāo)包括:

  • 停頓時(shí)間不超過(guò)10ms;

  • 停頓時(shí)間不會(huì)隨著堆的大小,或者活躍對(duì)象的大小而增加;

  • 支持8MB~4TB級(jí)別的堆(未來(lái)支持16TB)。

從設(shè)計(jì)目標(biāo)來(lái)看,我們知道ZGC適用于大內(nèi)存低延遲服務(wù)的內(nèi)存管理和回收。下面主要介紹ZGC在低延時(shí)場(chǎng)景中的應(yīng)用和卓越表現(xiàn)。

GC之痛

很多低延遲高可用Java服務(wù)的系統(tǒng)可用性經(jīng)常受GC停頓的困擾。GC停頓指垃圾回收期間STW(Stop The World),當(dāng)STW時(shí),所有應(yīng)用線(xiàn)程停止活動(dòng),等待GC停頓結(jié)束。以美團(tuán)風(fēng)控服務(wù)為例,部分上游業(yè)務(wù)要求風(fēng)控服務(wù)65ms內(nèi)返回結(jié)果,并且可用性要達(dá)到99.99%。但因?yàn)镚C停頓,我們未能達(dá)到上述可用性目標(biāo)。當(dāng)時(shí)使用的是CMS垃圾回收器,單次Young GC 40ms,一分鐘10次,接口平均響應(yīng)時(shí)間30ms。通過(guò)計(jì)算可知,有(40ms + 30ms) * 10次 / 60000ms = 1.12%的請(qǐng)求的響應(yīng)時(shí)間會(huì)增加0 ~ 40ms不等,其中30ms * 10次 / 60000ms = 0.5%的請(qǐng)求響應(yīng)時(shí)間會(huì)增加40ms??梢?jiàn),GC停頓對(duì)響應(yīng)時(shí)間的影響較大。為了降低GC停頓對(duì)系統(tǒng)可用性的影響,我們從降低單次GC時(shí)間和降低GC頻率兩個(gè)角度出發(fā)進(jìn)行了調(diào)優(yōu),還測(cè)試過(guò)G1垃圾回收器,但這三項(xiàng)措施均未能降低GC對(duì)服務(wù)可用性的影響。

CMS與G1停頓時(shí)間瓶頸

在介紹ZGC之前,首先回顧一下CMS和G1的GC過(guò)程以及停頓時(shí)間的瓶頸。CMS新生代的Young GC、G1和ZGC都基于標(biāo)記-復(fù)制算法,但算法具體實(shí)現(xiàn)的不同就導(dǎo)致了巨大的性能差異。

標(biāo)記-復(fù)制算法應(yīng)用在CMS新生代(ParNew是CMS默認(rèn)的新生代垃圾回收器)和G1垃圾回收器中。標(biāo)記-復(fù)制算法可以分為三個(gè)階段:

  • 標(biāo)記階段,即從GC Roots集合開(kāi)始,標(biāo)記活躍對(duì)象;

  • 轉(zhuǎn)移階段,即把活躍對(duì)象復(fù)制到新的內(nèi)存地址上;

  • 重定位階段,因?yàn)檗D(zhuǎn)移導(dǎo)致對(duì)象的地址發(fā)生了變化,在重定位階段,所有指向?qū)ο笈f地址的指針都要調(diào)整到對(duì)象新的地址上。

下面以G1為例,通過(guò)G1中標(biāo)記-復(fù)制算法過(guò)程(G1的Young GC和Mixed GC均采用該算法),分析G1停頓耗時(shí)的主要瓶頸。G1垃圾回收周期如下圖所示:

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

G1的混合回收過(guò)程可以分為標(biāo)記階段、清理階段和復(fù)制階段。

標(biāo)記階段停頓分析

  • 初始標(biāo)記階段:初始標(biāo)記階段是指從GC Roots出發(fā)標(biāo)記全部直接子節(jié)點(diǎn)的過(guò)程,該階段是STW的。由于GC Roots數(shù)量不多,通常該階段耗時(shí)非常短。

  • 并發(fā)標(biāo)記階段:并發(fā)標(biāo)記階段是指從GC Roots開(kāi)始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活對(duì)象。該階段是并發(fā)的,即應(yīng)用線(xiàn)程和GC線(xiàn)程可以同時(shí)活動(dòng)。并發(fā)標(biāo)記耗時(shí)相對(duì)長(zhǎng)很多,但因?yàn)椴皇荢TW,所以我們不太關(guān)心該階段耗時(shí)的長(zhǎng)短。

  • 再標(biāo)記階段:重新標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對(duì)象。該階段是STW的。

清理階段停頓分析

  • 清理階段清點(diǎn)出有存活對(duì)象的分區(qū)和沒(méi)有存活對(duì)象的分區(qū),該階段不會(huì)清理垃圾對(duì)象,也不會(huì)執(zhí)行存活對(duì)象的復(fù)制。該階段是STW的。

復(fù)制階段停頓分析

  • 復(fù)制算法中的轉(zhuǎn)移階段需要分配新內(nèi)存和復(fù)制對(duì)象的成員變量。轉(zhuǎn)移階段是STW的,其中內(nèi)存分配通常耗時(shí)非常短,但對(duì)象成員變量的復(fù)制耗時(shí)有可能較長(zhǎng),這是因?yàn)閺?fù)制耗時(shí)與存活對(duì)象數(shù)量與對(duì)象復(fù)雜度成正比。對(duì)象越復(fù)雜,復(fù)制耗時(shí)越長(zhǎng)。

四個(gè)STW過(guò)程中,初始標(biāo)記因?yàn)橹粯?biāo)記GC Roots,耗時(shí)較短。再標(biāo)記因?yàn)閷?duì)象數(shù)少,耗時(shí)也較短。清理階段因?yàn)閮?nèi)存分區(qū)數(shù)量少,耗時(shí)也較短。轉(zhuǎn)移階段要處理所有存活的對(duì)象,耗時(shí)會(huì)較長(zhǎng)。因此,G1停頓時(shí)間的瓶頸主要是標(biāo)記-復(fù)制中的轉(zhuǎn)移階段STW。為什么轉(zhuǎn)移階段不能和標(biāo)記階段一樣并發(fā)執(zhí)行呢?主要是G1未能解決轉(zhuǎn)移過(guò)程中準(zhǔn)確定位對(duì)象地址的問(wèn)題。

G1的Young GC和CMS的Young GC,其標(biāo)記-復(fù)制全過(guò)程STW,這里不再詳細(xì)闡述。

ZGC原理

全并發(fā)的ZGC

與CMS中的ParNew和G1類(lèi)似,ZGC也采用標(biāo)記-復(fù)制算法,不過(guò)ZGC對(duì)該算法做了重大改進(jìn):ZGC在標(biāo)記、轉(zhuǎn)移和重定位階段幾乎都是并發(fā)的,這是ZGC實(shí)現(xiàn)停頓時(shí)間小于10ms目標(biāo)的最關(guān)鍵原因。

ZGC垃圾回收周期如下圖所示:

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

ZGC只有三個(gè)STW階段:初始標(biāo)記再標(biāo)記,初始轉(zhuǎn)移。其中,初始標(biāo)記和初始轉(zhuǎn)移分別都只需要掃描所有GC Roots,其處理時(shí)間和GC Roots的數(shù)量成正比,一般情況耗時(shí)非常短;再標(biāo)記階段STW時(shí)間很短,最多1ms,超過(guò)1ms則再次進(jìn)入并發(fā)標(biāo)記階段。即,ZGC幾乎所有暫停都只依賴(lài)于GC Roots集合大小,停頓時(shí)間不會(huì)隨著堆的大小或者活躍對(duì)象的大小而增加。與ZGC對(duì)比,G1的轉(zhuǎn)移階段完全STW的,且停頓時(shí)間隨存活對(duì)象的大小增加而增加。

ZGC關(guān)鍵技術(shù)

ZGC通過(guò)著色指針和讀屏障技術(shù),解決了轉(zhuǎn)移過(guò)程中準(zhǔn)確訪(fǎng)問(wèn)對(duì)象的問(wèn)題,實(shí)現(xiàn)了并發(fā)轉(zhuǎn)移。大致原理描述如下:并發(fā)轉(zhuǎn)移中“并發(fā)”意味著GC線(xiàn)程在轉(zhuǎn)移對(duì)象的過(guò)程中,應(yīng)用線(xiàn)程也在不停地訪(fǎng)問(wèn)對(duì)象。假設(shè)對(duì)象發(fā)生轉(zhuǎn)移,但對(duì)象地址未及時(shí)更新,那么應(yīng)用線(xiàn)程可能訪(fǎng)問(wèn)到舊地址,從而造成錯(cuò)誤。而在ZGC中,應(yīng)用線(xiàn)程訪(fǎng)問(wèn)對(duì)象將觸發(fā)“讀屏障”,如果發(fā)現(xiàn)對(duì)象被移動(dòng)了,那么“讀屏障”會(huì)把讀出來(lái)的指針更新到對(duì)象的新地址上,這樣應(yīng)用線(xiàn)程始終訪(fǎng)問(wèn)的都是對(duì)象的新地址。那么,JVM是如何判斷對(duì)象被移動(dòng)過(guò)呢?就是利用對(duì)象引用的地址,即著色指針。下面介紹著色指針和讀屏障技術(shù)細(xì)節(jié)。

著色指針

著色指針是一種將信息存儲(chǔ)在指針中的技術(shù)。

ZGC僅支持64位系統(tǒng),它把64位虛擬地址空間劃分為多個(gè)子空間,如下圖所示:

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

其中,[0~4TB) 對(duì)應(yīng)Java堆,[4TB ~ 8TB) 稱(chēng)為M0地址空間,[8TB ~ 12TB) 稱(chēng)為M1地址空間,[12TB ~ 16TB) 預(yù)留未使用,[16TB ~ 20TB) 稱(chēng)為Remapped空間。

當(dāng)應(yīng)用程序創(chuàng)建對(duì)象時(shí),首先在堆空間申請(qǐng)一個(gè)虛擬地址,但該虛擬地址并不會(huì)映射到真正的物理地址。ZGC同時(shí)會(huì)為該對(duì)象在M0、M1和Remapped地址空間分別申請(qǐng)一個(gè)虛擬地址,且這三個(gè)虛擬地址對(duì)應(yīng)同一個(gè)物理地址,但這三個(gè)空間在同一時(shí)間有且只有一個(gè)空間有效。ZGC之所以設(shè)置三個(gè)虛擬地址空間,是因?yàn)樗褂谩翱臻g換時(shí)間”思想,去降低GC停頓時(shí)間。“空間換時(shí)間”中的空間是虛擬空間,而不是真正的物理空間。后續(xù)章節(jié)將詳細(xì)介紹這三個(gè)空間的切換過(guò)程。

與上述地址空間劃分相對(duì)應(yīng),ZGC實(shí)際僅使用64位地址空間的第0~41位,而第42~45位存儲(chǔ)元數(shù)據(jù),第47~63位固定為0。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

ZGC將對(duì)象存活信息存儲(chǔ)在42~45位中,這與傳統(tǒng)的垃圾回收并將對(duì)象存活信息放在對(duì)象頭中完全不同。

讀屏障

讀屏障是JVM向應(yīng)用代碼插入一小段代碼的技術(shù)。當(dāng)應(yīng)用線(xiàn)程從堆中讀取對(duì)象引用時(shí),就會(huì)執(zhí)行這段代碼。需要注意的是,僅“從堆中讀取對(duì)象引用”才會(huì)觸發(fā)這段代碼。

讀屏障示例:

Object o = obj.FieldA   // 從堆中讀取引用,需要加入屏障
<Load barrier>
Object p = o  // 無(wú)需加入屏障,因?yàn)椴皇菑亩阎凶x取引用
o.dosomething() // 無(wú)需加入屏障,因?yàn)椴皇菑亩阎凶x取引用
int i =  obj.FieldB  //無(wú)需加入屏障,因?yàn)椴皇菍?duì)象引用

ZGC中讀屏障的代碼作用:在對(duì)象標(biāo)記和轉(zhuǎn)移過(guò)程中,用于確定對(duì)象的引用地址是否滿(mǎn)足條件,并作出相應(yīng)動(dòng)作。

ZGC并發(fā)處理演示

接下來(lái)詳細(xì)介紹ZGC一次垃圾回收周期中地址視圖的切換過(guò)程:

  • 初始化:ZGC初始化之后,整個(gè)內(nèi)存空間的地址視圖被設(shè)置為Remapped。程序正常運(yùn)行,在內(nèi)存中分配對(duì)象,滿(mǎn)足一定條件后垃圾回收啟動(dòng),此時(shí)進(jìn)入標(biāo)記階段。

  • 并發(fā)標(biāo)記階段:第一次進(jìn)入標(biāo)記階段時(shí)視圖為M0,如果對(duì)象被GC標(biāo)記線(xiàn)程或者應(yīng)用線(xiàn)程訪(fǎng)問(wèn)過(guò),那么就將對(duì)象的地址視圖從Remapped調(diào)整為M0。所以,在標(biāo)記階段結(jié)束之后,對(duì)象的地址要么是M0視圖,要么是Remapped。如果對(duì)象的地址是M0視圖,那么說(shuō)明對(duì)象是活躍的;如果對(duì)象的地址是Remapped視圖,說(shuō)明對(duì)象是不活躍的。

  • 并發(fā)轉(zhuǎn)移階段:標(biāo)記結(jié)束后就進(jìn)入轉(zhuǎn)移階段,此時(shí)地址視圖再次被設(shè)置為Remapped。如果對(duì)象被GC轉(zhuǎn)移線(xiàn)程或者應(yīng)用線(xiàn)程訪(fǎng)問(wèn)過(guò),那么就將對(duì)象的地址視圖從M0調(diào)整為Remapped。

其實(shí),在標(biāo)記階段存在兩個(gè)地址視圖M0和M1,上面的過(guò)程顯示只用了一個(gè)地址視圖。之所以設(shè)計(jì)成兩個(gè),是為了區(qū)別前一次標(biāo)記和當(dāng)前標(biāo)記。也即,第二次進(jìn)入并發(fā)標(biāo)記階段后,地址視圖調(diào)整為M1,而非M0。

著色指針和讀屏障技術(shù)不僅應(yīng)用在并發(fā)轉(zhuǎn)移階段,還應(yīng)用在并發(fā)標(biāo)記階段:將對(duì)象設(shè)置為已標(biāo)記,傳統(tǒng)的垃圾回收器需要進(jìn)行一次內(nèi)存訪(fǎng)問(wèn),并將對(duì)象存活信息放在對(duì)象頭中;而在ZGC中,只需要設(shè)置指針地址的第42~45位即可,并且因?yàn)槭羌拇嫫髟L(fǎng)問(wèn),所以速度比訪(fǎng)問(wèn)內(nèi)存更快。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

ZGC調(diào)優(yōu)實(shí)踐

ZGC不是“銀彈”,需要根據(jù)服務(wù)的具體特點(diǎn)進(jìn)行調(diào)優(yōu)。網(wǎng)絡(luò)上能搜索到實(shí)戰(zhàn)經(jīng)驗(yàn)較少,調(diào)優(yōu)理論需自行摸索,我們?cè)诖穗A段也耗費(fèi)了不少時(shí)間,最終才達(dá)到理想的性能。本文的一個(gè)目的是列舉一些使用ZGC時(shí)常見(jiàn)的問(wèn)題,幫助大家使用ZGC提高服務(wù)可用性。

調(diào)優(yōu)基礎(chǔ)知識(shí)

理解ZGC重要配置參數(shù)

以我們服務(wù)在生產(chǎn)環(huán)境中ZGC參數(shù)配置為例,說(shuō)明各個(gè)參數(shù)的作用:

重要參數(shù)配置樣例:

-Xms10G -Xmx10G 
-XX:ReservedCodeCacheSize=256m -XX:InitialCodeCacheSize=256m 
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC 
-XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 
-XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive 
-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/opt/logs/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m

-Xms -Xmx:堆的最大內(nèi)存和最小內(nèi)存,這里都設(shè)置為10G,程序的堆內(nèi)存將保持10G不變。 -XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize:設(shè)置CodeCache的大小, JIT編譯的代碼都放在CodeCache中,一般服務(wù)64m或128m就已經(jīng)足夠。我們的服務(wù)因?yàn)橛幸欢ㄌ厥庑?,所以設(shè)置的較大,后面會(huì)詳細(xì)介紹。 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC:?jiǎn)⒂肸GC的配置。 -XX:ConcGCThreads:并發(fā)回收垃圾的線(xiàn)程。默認(rèn)是總核數(shù)的12.5%,8核CPU默認(rèn)是1。調(diào)大后GC變快,但會(huì)占用程序運(yùn)行時(shí)的CPU資源,吞吐會(huì)受到影響。 -XX:ParallelGCThreads:STW階段使用線(xiàn)程數(shù),默認(rèn)是總核數(shù)的60%。 -XX:ZCollectionInterval:ZGC發(fā)生的最小時(shí)間間隔,單位秒。 -XX:ZAllocationSpikeTolerance:ZGC觸發(fā)自適應(yīng)算法的修正系數(shù),默認(rèn)2,數(shù)值越大,越早的觸發(fā)ZGC。 -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否啟用主動(dòng)回收,默認(rèn)開(kāi)啟,這里的配置表示關(guān)閉。 -Xlog:設(shè)置GC日志中的內(nèi)容、格式、位置以及每個(gè)日志的大小。

理解ZGC觸發(fā)時(shí)機(jī)

相比于CMS和G1的GC觸發(fā)機(jī)制,ZGC的GC觸發(fā)機(jī)制有很大不同。ZGC的核心特點(diǎn)是并發(fā),GC過(guò)程中一直有新的對(duì)象產(chǎn)生。如何保證在GC完成之前,新產(chǎn)生的對(duì)象不會(huì)將堆占滿(mǎn),是ZGC參數(shù)調(diào)優(yōu)的第一大目標(biāo)。因?yàn)樵赯GC中,當(dāng)垃圾來(lái)不及回收將堆占滿(mǎn)時(shí),會(huì)導(dǎo)致正在運(yùn)行的線(xiàn)程停頓,持續(xù)時(shí)間可能長(zhǎng)達(dá)秒級(jí)之久。

ZGC有多種GC觸發(fā)機(jī)制,總結(jié)如下:

  • 阻塞內(nèi)存分配請(qǐng)求觸發(fā):當(dāng)垃圾來(lái)不及回收,垃圾將堆占滿(mǎn)時(shí),會(huì)導(dǎo)致部分線(xiàn)程阻塞。我們應(yīng)當(dāng)避免出現(xiàn)這種觸發(fā)方式。日志中關(guān)鍵字是“Allocation Stall”。

  • 基于分配速率的自適應(yīng)算法:最主要的GC觸發(fā)方式,其算法原理可簡(jiǎn)單描述為”ZGC根據(jù)近期的對(duì)象分配速率以及GC時(shí)間,計(jì)算出當(dāng)內(nèi)存占用達(dá)到什么閾值時(shí)觸發(fā)下一次GC”。自適應(yīng)算法的詳細(xì)理論可參考彭成寒《新一代垃圾回收器ZGC設(shè)計(jì)與實(shí)現(xiàn)》一書(shū)中的內(nèi)容。通過(guò)ZAllocationSpikeTolerance參數(shù)控制閾值大小,該參數(shù)默認(rèn)2,數(shù)值越大,越早的觸發(fā)GC。我們通過(guò)調(diào)整此參數(shù)解決了一些問(wèn)題。日志中關(guān)鍵字是“Allocation Rate”。

  • 基于固定時(shí)間間隔:通過(guò)ZCollectionInterval控制,適合應(yīng)對(duì)突增流量場(chǎng)景。流量平穩(wěn)變化時(shí),自適應(yīng)算法可能在堆使用率達(dá)到95%以上才觸發(fā)GC。流量突增時(shí),自適應(yīng)算法觸發(fā)的時(shí)機(jī)可能會(huì)過(guò)晚,導(dǎo)致部分線(xiàn)程阻塞。我們通過(guò)調(diào)整此參數(shù)解決流量突增場(chǎng)景的問(wèn)題,比如定時(shí)活動(dòng)、秒殺等場(chǎng)景。日志中關(guān)鍵字是“Timer”。

  • 主動(dòng)觸發(fā)規(guī)則:類(lèi)似于固定間隔規(guī)則,但時(shí)間間隔不固定,是ZGC自行算出來(lái)的時(shí)機(jī),我們的服務(wù)因?yàn)橐呀?jīng)加了基于固定時(shí)間間隔的觸發(fā)機(jī)制,所以通過(guò)-ZProactive參數(shù)將該功能關(guān)閉,以免GC頻繁,影響服務(wù)可用性。 日志中關(guān)鍵字是“Proactive”。

  • 預(yù)熱規(guī)則:服務(wù)剛啟動(dòng)時(shí)出現(xiàn),一般不需要關(guān)注。日志中關(guān)鍵字是“Warmup”。

  • 外部觸發(fā):代碼中顯式調(diào)用System.gc()觸發(fā)。 日志中關(guān)鍵字是“System.gc()”。

  • 元數(shù)據(jù)分配觸發(fā):元數(shù)據(jù)區(qū)不足時(shí)導(dǎo)致,一般不需要關(guān)注。 日志中關(guān)鍵字是“Metadata GC Threshold”。

理解ZGC日志

一次完整的GC過(guò)程,需要注意的點(diǎn)已在圖中標(biāo)出。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

注意:該日志過(guò)濾了進(jìn)入安全點(diǎn)的信息。正常情況,在一次GC過(guò)程中還穿插著進(jìn)入安全點(diǎn)的操作。

GC日志中每一行都注明了GC過(guò)程中的信息,關(guān)鍵信息如下:

  • Start:開(kāi)始GC,并標(biāo)明的GC觸發(fā)的原因。上圖中觸發(fā)原因是自適應(yīng)算法。

  • Phase-Pause Mark Start:初始標(biāo)記,會(huì)STW。

  • Phase-Pause Mark End:再次標(biāo)記,會(huì)STW。

  • Phase-Pause Relocate Start:初始轉(zhuǎn)移,會(huì)STW。

  • Heap信息:記錄了GC過(guò)程中Mark、Relocate前后的堆大小變化狀況。High和Low記錄了其中的最大值和最小值,我們一般關(guān)注High中Used的值,如果達(dá)到100%,在GC過(guò)程中一定存在內(nèi)存分配不足的情況,需要調(diào)整GC的觸發(fā)時(shí)機(jī),更早或者更快地進(jìn)行GC。

  • GC信息統(tǒng)計(jì):可以定時(shí)的打印垃圾收集信息,觀察10秒內(nèi)、10分鐘內(nèi)、10個(gè)小時(shí)內(nèi),從啟動(dòng)到現(xiàn)在的所有統(tǒng)計(jì)信息。利用這些統(tǒng)計(jì)信息,可以排查定位一些異常點(diǎn)。

日志中內(nèi)容較多,關(guān)鍵點(diǎn)已用紅線(xiàn)標(biāo)出,含義較好理解,更詳細(xì)的解釋大家可以自行在網(wǎng)上查閱資料。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

理解ZGC停頓原因

我們?cè)趯?shí)戰(zhàn)過(guò)程中共發(fā)現(xiàn)了6種使程序停頓的場(chǎng)景,分別如下:

  • GC時(shí),初始標(biāo)記:日志中Pause Mark Start。

  • GC時(shí),再標(biāo)記:日志中Pause Mark End。

  • GC時(shí),初始轉(zhuǎn)移:日志中Pause Relocate Start。

  • 內(nèi)存分配阻塞:當(dāng)內(nèi)存不足時(shí)線(xiàn)程會(huì)阻塞等待GC完成,關(guān)鍵字是"Allocation Stall"。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

  • 安全點(diǎn):所有線(xiàn)程進(jìn)入到安全點(diǎn)后才能進(jìn)行GC,ZGC定期進(jìn)入安全點(diǎn)判斷是否需要GC。先進(jìn)入安全點(diǎn)的線(xiàn)程需要等待后進(jìn)入安全點(diǎn)的線(xiàn)程直到所有線(xiàn)程掛起。

  • dump線(xiàn)程、內(nèi)存:比如jstack、jmap命令。

怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐 怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐

調(diào)優(yōu)案例

我們維護(hù)的服務(wù)名叫Zeus,它是美團(tuán)的規(guī)則平臺(tái),常用于風(fēng)控場(chǎng)景中的規(guī)則管理。規(guī)則運(yùn)行是基于開(kāi)源的表達(dá)式執(zhí)行引擎Aviator。Aviator內(nèi)部將每一條表達(dá)式轉(zhuǎn)化成Java的一個(gè)類(lèi),通過(guò)調(diào)用該類(lèi)的接口實(shí)現(xiàn)表達(dá)式邏輯。

Zeus服務(wù)內(nèi)的規(guī)則數(shù)量超過(guò)萬(wàn)條,且每臺(tái)機(jī)器每天的請(qǐng)求量幾百萬(wàn)。這些客觀條件導(dǎo)致Aviator生成的類(lèi)和方法會(huì)產(chǎn)生很多的ClassLoader和CodeCache,這些在使用ZGC時(shí)都成為過(guò)GC的性能瓶頸。接下來(lái)介紹兩類(lèi)調(diào)優(yōu)案例。

內(nèi)存分配阻塞,系統(tǒng)停頓可達(dá)到秒級(jí)

案例一:秒殺活動(dòng)中流量突增,出現(xiàn)性能毛刺

日志信息:對(duì)比出現(xiàn)性能毛刺時(shí)間點(diǎn)的GC日志和業(yè)務(wù)日志,發(fā)現(xiàn)JVM停頓了較長(zhǎng)時(shí)間,且停頓時(shí)GC日志中有大量的“Allocation Stall”日志。

分析:這種案例多出現(xiàn)在“自適應(yīng)算法”為主要GC觸發(fā)機(jī)制的場(chǎng)景中。ZGC是一款并發(fā)的垃圾回收器,GC線(xiàn)程和應(yīng)用線(xiàn)程同時(shí)活動(dòng),在GC過(guò)程中,還會(huì)產(chǎn)生新的對(duì)象。GC完成之前,新產(chǎn)生的對(duì)象將堆占滿(mǎn),那么應(yīng)用線(xiàn)程可能因?yàn)樯暾?qǐng)內(nèi)存失敗而導(dǎo)致線(xiàn)程阻塞。當(dāng)秒殺活動(dòng)開(kāi)始,大量請(qǐng)求打入系統(tǒng),但自適應(yīng)算法計(jì)算的GC觸發(fā)間隔較長(zhǎng),導(dǎo)致GC觸發(fā)不及時(shí),引起了內(nèi)存分配阻塞,導(dǎo)致停頓。

解決方法:

(1)開(kāi)啟”基于固定時(shí)間間隔“的GC觸發(fā)機(jī)制:-XX:ZCollectionInterval。比如調(diào)整為5秒,甚至更短。
(2)增大修正系數(shù)-XX:ZAllocationSpikeTolerance,更早觸發(fā)GC。ZGC采用正態(tài)分布模型預(yù)測(cè)內(nèi)存分配速率,模型修正系數(shù)ZAllocationSpikeTolerance默認(rèn)值為2,值越大,越早的觸發(fā)GC,Zeus中所有集群設(shè)置的是5。

案例二:壓測(cè)時(shí),流量逐漸增大到一定程度后,出現(xiàn)性能毛刺

日志信息:平均1秒GC一次,兩次GC之間幾乎沒(méi)有間隔。

分析:GC觸發(fā)及時(shí),但內(nèi)存標(biāo)記和回收速度過(guò)慢,引起內(nèi)存分配阻塞,導(dǎo)致停頓。

解決方法:增大-XX:ConcGCThreads, 加快并發(fā)標(biāo)記和回收速度。ConcGCThreads默認(rèn)值是核數(shù)的1/8,8核機(jī)器,默認(rèn)值是1。該參數(shù)影響系統(tǒng)吞吐,如果GC間隔時(shí)間大于GC周期,不建議調(diào)整該參數(shù)。

GC Roots 數(shù)量大,單次GC停頓時(shí)間長(zhǎng)

案例三: 單次GC停頓時(shí)間30ms,與預(yù)期停頓10ms左右有較大差距

日志信息:觀察ZGC日志信息統(tǒng)計(jì),“Pause Roots ClassLoaderDataGraph”一項(xiàng)耗時(shí)較長(zhǎng)。

分析:dump內(nèi)存文件,發(fā)現(xiàn)系統(tǒng)中有上萬(wàn)個(gè)ClassLoader實(shí)例。我們知道ClassLoader屬于GC Roots一部分,且ZGC停頓時(shí)間與GC Roots成正比,GC Roots數(shù)量越大,停頓時(shí)間越久。再進(jìn)一步分析,ClassLoader的類(lèi)名表明,這些ClassLoader均由Aviator組件生成。分析Aviator源碼,發(fā)現(xiàn)Aviator對(duì)每一個(gè)表達(dá)式新生成類(lèi)時(shí),會(huì)創(chuàng)建一個(gè)ClassLoader,這導(dǎo)致了ClassLoader數(shù)量巨大的問(wèn)題。在更高Aviator版本中,該問(wèn)題已經(jīng)被修復(fù),即僅創(chuàng)建一個(gè)ClassLoader為所有表達(dá)式生成類(lèi)。

解決方法:升級(jí)Aviator組件版本,避免生成多余的ClassLoader。

案例四:服務(wù)啟動(dòng)后,運(yùn)行時(shí)間越長(zhǎng),單次GC時(shí)間越長(zhǎng),重啟后恢復(fù)

日志信息:觀察ZGC日志信息統(tǒng)計(jì),“Pause Roots CodeCache”的耗時(shí)會(huì)隨著服務(wù)運(yùn)行時(shí)間逐漸增長(zhǎng)。

分析:CodeCache空間用于存放Java熱點(diǎn)代碼的JIT編譯結(jié)果,而CodeCache也屬于GC Roots一部分。通過(guò)添加-XX:+PrintCodeCacheOnCompilation參數(shù),打印CodeCache中的被優(yōu)化的方法,發(fā)現(xiàn)大量的Aviator表達(dá)式代碼。定位到根本原因,每個(gè)表達(dá)式都是一個(gè)類(lèi)中一個(gè)方法。隨著運(yùn)行時(shí)間越長(zhǎng),執(zhí)行次數(shù)增加,這些方法會(huì)被JIT優(yōu)化編譯進(jìn)入到Code Cache中,導(dǎo)致CodeCache越來(lái)越大。

解決方法:JIT有一些參數(shù)配置可以調(diào)整JIT編譯的條件,但對(duì)于我們的問(wèn)題都不太適用。我們最終通過(guò)業(yè)務(wù)優(yōu)化解決,刪除不需要執(zhí)行的Aviator表達(dá)式,從而避免了大量Aviator方法進(jìn)入CodeCache中。

值得一提的是,我們并不是在所有這些問(wèn)題都解決后才全量部署所有集群。即使開(kāi)始有各種各樣的毛刺,但計(jì)算后發(fā)現(xiàn),有各種問(wèn)題的ZGC也比之前的CMS對(duì)服務(wù)可用性影響小。所以從開(kāi)始準(zhǔn)備使用ZGC到全量部署,大概用了2周的時(shí)間。在之后的3個(gè)月時(shí)間里,我們邊做業(yè)務(wù)需求,邊跟進(jìn)這些問(wèn)題,最終逐個(gè)解決了上述問(wèn)題,從而使ZGC在各個(gè)集群上達(dá)到了一個(gè)更好表現(xiàn)。

升級(jí)ZGC效果

延遲降低

TP(Top Percentile)是一項(xiàng)衡量系統(tǒng)延遲的指標(biāo):TP999表示99.9%請(qǐng)求都能被響應(yīng)的最小耗時(shí);TP99表示99%請(qǐng)求都能被響應(yīng)的最小耗時(shí)。

在Zeus服務(wù)不同集群中,ZGC在低延遲(TP999 < 200ms)場(chǎng)景中收益較大:

  • TP999:下降12~142ms,下降幅度18%~74%。

  • TP99:下降5~28ms,下降幅度10%~47%。

超低延遲(TP999 < 20ms)和高延遲(TP999 > 200ms)服務(wù)收益不大,原因是這些服務(wù)的響應(yīng)時(shí)間瓶頸不是GC,而是外部依賴(lài)的性能。

吞吐下降

對(duì)吞吐量?jī)?yōu)先的場(chǎng)景,ZGC可能并不適合。例如,Zeus某離線(xiàn)集群原先使用CMS,升級(jí)ZGC后,系統(tǒng)吞吐量明顯降低。究其原因有二:第一,ZGC是單代垃圾回收器,而CMS是分代垃圾回收器。單代垃圾回收器每次處理的對(duì)象更多,更耗費(fèi)CPU資源;第二,ZGC使用讀屏障,讀屏障操作需耗費(fèi)額外的計(jì)算資源。

總結(jié)

ZGC作為下一代垃圾回收器,性能非常優(yōu)秀。ZGC垃圾回收過(guò)程幾乎全部是并發(fā),實(shí)際STW停頓時(shí)間極短,不到10ms。這得益于其采用的著色指針和讀屏障技術(shù)。

Zeus在升級(jí)JDK 11+ZGC中,通過(guò)將風(fēng)險(xiǎn)和問(wèn)題分類(lèi),然后各個(gè)擊破,最終順利實(shí)現(xiàn)了升級(jí)目標(biāo),GC停頓也幾乎不再影響系統(tǒng)可用性。

最后推薦大家升級(jí)ZGC,Zeus系統(tǒng)因?yàn)闃I(yè)務(wù)特點(diǎn),遇到了較多問(wèn)題,而風(fēng)控其他團(tuán)隊(duì)在升級(jí)時(shí)都非常順利。歡迎大家加入“ZGC使用交流”群。

參考文獻(xiàn)

  • ZGC官網(wǎng)

  • 彭成寒.《新一代垃圾回收器ZGC設(shè)計(jì)與實(shí)現(xiàn)》. 機(jī)械工業(yè)出版社, 2019.

  • 從實(shí)際案例聊聊Java應(yīng)用的GC優(yōu)化

  • Java Hotspot G1 GC的一些關(guān)鍵技術(shù)

附錄

如何使用新技術(shù)

在生產(chǎn)環(huán)境升級(jí)JDK 11,使用ZGC,大家最關(guān)心的可能不是效果怎么樣,而是這個(gè)新版本用的人少,網(wǎng)上實(shí)踐也少,靠不靠譜,穩(wěn)不穩(wěn)定。其次是升級(jí)成本會(huì)不會(huì)很大,萬(wàn)一不成功豈不是白白浪費(fèi)時(shí)間。所以,在使用新技術(shù)前,首先要做的是評(píng)估收益、成本和風(fēng)險(xiǎn)。

評(píng)估收益

對(duì)于JDK這種世界關(guān)注的程序,大版本升級(jí)所引入的新技術(shù)一般已經(jīng)在理論上經(jīng)過(guò)驗(yàn)證。我們要做的事情就是確定當(dāng)前系統(tǒng)的瓶頸是否是新版本JDK可解決的問(wèn)題,切忌問(wèn)題未診斷清楚就采取措施。評(píng)估完收益之后再評(píng)估成本和風(fēng)險(xiǎn),收益過(guò)大或者過(guò)小,其他兩項(xiàng)影響權(quán)重就會(huì)小很多。

以本文開(kāi)頭提到的案例為例,假設(shè)GC次數(shù)不變(10次/分鐘),且單次GC時(shí)間從40ms降低10ms。通過(guò)計(jì)算,一分鐘內(nèi)有100/60000 = 0.17%的時(shí)間在進(jìn)行GC,且期間所有請(qǐng)求僅停頓10ms,GC期間影響的請(qǐng)求數(shù)和因GC增加的延遲都有所減少。

評(píng)估成本

這里主要指升級(jí)所需要的人力成本。此項(xiàng)相對(duì)比較成熟,根據(jù)新技術(shù)的使用手冊(cè)判斷改動(dòng)點(diǎn)。跟做其他項(xiàng)目區(qū)別不大,不再具體細(xì)說(shuō)。

在我們的實(shí)踐中,兩周時(shí)間完成線(xiàn)上部署,達(dá)到安全穩(wěn)定運(yùn)行的狀態(tài)。后續(xù)持續(xù)迭代3個(gè)月,根據(jù)業(yè)務(wù)場(chǎng)景對(duì)ZGC進(jìn)行了更契合的優(yōu)化適配。

評(píng)估風(fēng)險(xiǎn)

升級(jí)JDK的風(fēng)險(xiǎn)可以分為三類(lèi):

  • 兼容性風(fēng)險(xiǎn):Java程序JAR包依賴(lài)很多,升級(jí)JDK版本后程序是否能運(yùn)行起來(lái)。例如我們的服務(wù)是從JDK 7升級(jí)到JDK 11,需要解決較多JAR包不兼容的問(wèn)題。

  • 功能風(fēng)險(xiǎn):運(yùn)行起來(lái)后,是否會(huì)有一些組件邏輯變更,影響現(xiàn)有功能的邏輯。

  • 性能風(fēng)險(xiǎn):功能如果沒(méi)有問(wèn)題,性能是否穩(wěn)定,能穩(wěn)定的在線(xiàn)上運(yùn)行。

經(jīng)過(guò)分類(lèi)后,每類(lèi)風(fēng)險(xiǎn)的應(yīng)對(duì)轉(zhuǎn)化成了常見(jiàn)的測(cè)試問(wèn)題,不再屬于未知風(fēng)險(xiǎn)。風(fēng)險(xiǎn)是指不確定的事情,如果不確定的事情都能轉(zhuǎn)化成可確定的事情,意味著風(fēng)險(xiǎn)已消除。

升級(jí)JDK 11

選擇JDK 11,是因?yàn)樵贘DK 11中首次支持ZGC,而且JDK 11屬于長(zhǎng)期支持(Long Term Support,LTS)版本,至少會(huì)被維護(hù)三年,普通版本(如JDK 12、JDK 13和JDK 14)只有6個(gè)月的維護(hù)周期,不建議使用。

本地測(cè)試環(huán)境安裝

從兩個(gè)源OpenJDK和OracleJDK 下載JDK 11,二個(gè)版本的JDK主要區(qū)別是長(zhǎng)時(shí)期的免費(fèi)和付費(fèi),短期內(nèi)都免費(fèi)。注意JDK 11版本中的ZGC不支持Mac OS系統(tǒng),在Mac OS系統(tǒng)上使用JDK 11只能用其他垃圾回收器,如G1。

生產(chǎn)環(huán)境安裝

升級(jí)JDK 11不僅僅是升級(jí)自己項(xiàng)目的JDK版本,還需要編譯、發(fā)布部署、運(yùn)行、監(jiān)控、性能內(nèi)存分析工具等項(xiàng)目支持。美團(tuán)內(nèi)部的實(shí)踐:

編譯打包:美團(tuán)發(fā)布系統(tǒng)支持選擇JDK 11進(jìn)行編譯打包。 線(xiàn)上運(yùn)行 & 全量部署:要求線(xiàn)上機(jī)器已安裝JDK11,有3種方式:

1.新申請(qǐng)默認(rèn)安裝JDK 11的虛擬機(jī):試用JDK 11時(shí)可用這種方式;全量部署時(shí),如果新申請(qǐng)機(jī)器數(shù)量過(guò)多,可能沒(méi)有足夠機(jī)器資源。 2.通過(guò)手寫(xiě)腳本給存量虛擬機(jī)安裝JDK 11:不推薦,業(yè)務(wù)同學(xué)過(guò)多參與到運(yùn)維當(dāng)中。 3.使用容器提供的鏡像部署功能,在打包鏡像時(shí)安裝JDK 11:推薦方式,不需要新申請(qǐng)資源。

監(jiān)控指標(biāo):主要是GC的時(shí)間和頻率,我們通過(guò)美團(tuán)的CAT監(jiān)控系統(tǒng)支持ZGC數(shù)據(jù)的收集(CAT已開(kāi)源)。 性能內(nèi)存分析:線(xiàn)上遇到性能問(wèn)題時(shí),還需要借助Profiling工具,美團(tuán)的性能診斷優(yōu)化平臺(tái)Scalpel已支持JDK 11的性能內(nèi)存分析。如果你的公司沒(méi)有相關(guān)工具,推薦使用JProfier。

解決組件兼容性

我們的項(xiàng)目包含二十多萬(wàn)行代碼,需要從JDK 7升級(jí)到JDK 11,依賴(lài)組件眾多。雖然看起來(lái)升級(jí)會(huì)比較復(fù)雜,但實(shí)際只花了兩天時(shí)間即解決了兼容性問(wèn)題。具體過(guò)程如下:

1.編譯,需要修改pom文件中的build配置,根據(jù)報(bào)錯(cuò)作修改,主要有兩類(lèi):

a.一些類(lèi)被刪除:比如“sun.misc.BASE64Encoder”,找到替換類(lèi)java.util.Base64即可。

b.組件依賴(lài)版本不兼容JDK 11問(wèn)題:找到對(duì)應(yīng)依賴(lài)組件,搜索最新版本,一般都支持JDK 11。

2.編譯成功后,啟動(dòng)運(yùn)行,此時(shí)仍有可能組件依賴(lài)版本問(wèn)題,按照編譯時(shí)的方式處理即可。

升級(jí)所修改的依賴(lài):

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.4</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-parent</artifactId>
    <version>6.0.16.Final</version>
</dependency>
<dependency>
    <groupId>com.sankuai.inf</groupId>
    <artifactId>patriot-sdk</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.39.Final</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

JDK 11已經(jīng)出來(lái)兩年,常見(jiàn)的依賴(lài)組件都有兼容性版本。但是,如果是公司內(nèi)部提供的公司級(jí)組件,可能會(huì)不兼容JDK 11,需要推動(dòng)相關(guān)組件進(jìn)行升級(jí)。如果對(duì)方升級(jí)較為困難,可以考慮拆分功能,將依賴(lài)這些組件的功能單獨(dú)部署,繼續(xù)使用低版本JDK。隨著JDK11的卓越性能被大家悉知,相信會(huì)有更多團(tuán)隊(duì)會(huì)用JDK 11解決GC問(wèn)題,使用者越多,各個(gè)組件升級(jí)的動(dòng)力也會(huì)越大。

驗(yàn)證功能正確性

通過(guò)完備的單測(cè)、集成和回歸測(cè)試,保證功能正確性。

看完上述內(nèi)容,你們對(duì)怎么進(jìn)行新一代垃圾回收器ZGC的探索與實(shí)踐有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

zgc
AI