文章來源于公眾號美團技術(shù)團隊 可婶,作者王東 王偉
作者簡介
王東沿癞,美團信息安全資深工程師。
王偉矛渴,美團信息安全技術(shù)專家抛寝。
ZGC(The Z Garbage Collector)是JDK 11中推出的一款低延遲垃圾回收器,它的設(shè)計目標包括:
停頓時間不超過10ms曙旭;
停頓時間不會隨著堆的大小盗舰,或者活躍對象的大小而增加;
支持8MB~4TB級別的堆(未來支持16TB)桂躏。
從設(shè)計目標來看钻趋,我們知道ZGC適用于大內(nèi)存低延遲服務(wù)的內(nèi)存管理和回收。本文主要介紹ZGC在低延時場景中的應(yīng)用和卓越表現(xiàn)剂习,文章內(nèi)容主要分為四部分:
- GC之痛:介紹實際業(yè)務(wù)中遇到的GC痛點蛮位,并分析CMS收集器和G1收集器停頓時間瓶頸;
- ZGC原理:分析ZGC停頓時間比G1或CMS更短的本質(zhì)原因鳞绕,以及背后的技術(shù)原理失仁;
- ZGC調(diào)優(yōu)實踐:重點分享對ZGC調(diào)優(yōu)的理解,并分析若干個實際調(diào)優(yōu)案例们何;
- 升級ZGC效果:展示在生產(chǎn)環(huán)境應(yīng)用ZGC取得的效果萄焦。
GC之痛
很多低延遲高可用Java服務(wù)的系統(tǒng)可用性經(jīng)常受GC停頓的困擾。GC停頓指垃圾回收期間STW(Stop The World)冤竹,當(dāng)STW時拂封,所有應(yīng)用線程停止活動,等待GC停頓結(jié)束鹦蠕。
以美團風(fēng)控服務(wù)為例冒签,部分上游業(yè)務(wù)要求風(fēng)控服務(wù)65ms內(nèi)返回結(jié)果,并且可用性要達到99.99%钟病。但因為GC停頓萧恕,我們未能達到上述可用性目標。當(dāng)時使用的是CMS垃圾回收器肠阱,單次Young GC 40ms票唆,一分鐘10次,接口平均響應(yīng)時間30ms辖所。通過計算可知惰说,有( 40ms + 30ms ) * 10次 / 60000ms = 1.12%的請求的響應(yīng)時間會增加0 ~ 40ms不等,其中30ms * 10次 / 60000ms = 0.5%的請求響應(yīng)時間會增加40ms缘回。
可見吆视,GC停頓對響應(yīng)時間的影響較大典挑。為了降低GC停頓對系統(tǒng)可用性的影響,我們從降低單次GC時間和降低GC頻率兩個角度出發(fā)進行了調(diào)優(yōu)啦吧,還測試過G1垃圾回收器您觉,但這三項措施均未能降低GC對服務(wù)可用性的影響。
CMS與G1停頓時間瓶頸
在介紹ZGC之前授滓,首先回顧一下CMS和G1的GC過程以及停頓時間的瓶頸琳水。CMS新生代的Young GC、G1和ZGC都基于標記-復(fù)制算法般堆,但算法具體實現(xiàn)的不同就導(dǎo)致了巨大的性能差異在孝。
標記-復(fù)制算法應(yīng)用在CMS新生代(ParNew是CMS默認的新生代垃圾回收器)和G1垃圾回收器中。標記-復(fù)制算法可以分為三個階段:
- 標記階段淮摔,即從GC Roots集合開始私沮,標記活躍對象;
- 轉(zhuǎn)移階段和橙,即把活躍對象復(fù)制到新的內(nèi)存地址上仔燕;
- 重定位階段,因為轉(zhuǎn)移導(dǎo)致對象的地址發(fā)生了變化魔招,在重定位階段晰搀,所有指向?qū)ο笈f地址的指針都要調(diào)整到對象新的地址上。
下面以G1為例办斑,通過G1中標記-復(fù)制算法過程(G1的Young GC和Mixed GC均采用該算法)外恕,分析G1停頓耗時的主要瓶頸。G1垃圾回收周期如下圖所示:
G1的混合回收過程可以分為標記階段俄周、清理階段和復(fù)制階段吁讨。
標記階段停頓分析
- 初始標記階段:初始標記階段是指從GC Roots出發(fā)標記全部直接子節(jié)點的過程,該階段是STW的峦朗。由于GC Roots數(shù)量不多,通常該階段耗時非常短排龄。
- 并發(fā)標記階段:并發(fā)標記階段是指從GC Roots開始對堆中對象進行可達性分析波势,找出存活對象。該階段是并發(fā)的橄维,即應(yīng)用線程和GC線程可以同時活動尺铣。并發(fā)標記耗時相對長很多,但因為不是STW争舞,所以我們不太關(guān)心該階段耗時的長短凛忿。
- 再標記階段:重新標記那些在并發(fā)標記階段發(fā)生變化的對象。該階段是STW的竞川。
清理階段停頓分析
- 清理階段清點出有存活對象的分區(qū)和沒有存活對象的分區(qū)店溢,該階段不會清理垃圾對象叁熔,也不會執(zhí)行存活對象的復(fù)制。該階段是STW的床牧。
復(fù)制階段停頓分析
- 復(fù)制算法中的轉(zhuǎn)移階段需要分配新內(nèi)存和復(fù)制對象的成員變量荣回。轉(zhuǎn)移階段是STW的,其中內(nèi)存分配通常耗時非常短戈咳,但對象成員變量的復(fù)制耗時有可能較長心软,這是因為復(fù)制耗時與存活對象數(shù)量與對象復(fù)雜度成正比。對象越復(fù)雜著蛙,復(fù)制耗時越長删铃。
四個STW過程中,初始標記因為只標記GC Roots踏堡,耗時較短泳姐。再標記因為對象數(shù)少,耗時也較短暂吉。清理階段因為內(nèi)存分區(qū)數(shù)量少胖秒,耗時也較短。轉(zhuǎn)移階段要處理所有存活的對象慕的,耗時會較長阎肝。因此,G1停頓時間的瓶頸主要是標記-復(fù)制中的轉(zhuǎn)移階段STW肮街。為什么轉(zhuǎn)移階段不能和標記階段一樣并發(fā)執(zhí)行呢风题?主要是G1未能解決轉(zhuǎn)移過程中準確定位對象地址的問題。
G1的Young GC和CMS的Young GC嫉父,其標記-復(fù)制全過程STW沛硅,這里不再詳細闡述。
ZGC原理
全并發(fā)的ZGC
與CMS中的ParNew和G1類似绕辖,ZGC也采用標記-復(fù)制算法摇肌,不過ZGC對該算法做了重大改進:ZGC在標記、轉(zhuǎn)移和重定位階段幾乎都是并發(fā)的仪际,這是ZGC實現(xiàn)停頓時間小于10ms目標的最關(guān)鍵原因围小。
ZGC垃圾回收周期如下圖所示:
ZGC只有三個STW階段:初始標記,再標記树碱,初始轉(zhuǎn)移肯适。其中,初始標記和初始轉(zhuǎn)移分別都只需要掃描所有GC Roots成榜,其處理時間和GC Roots的數(shù)量成正比框舔,一般情況耗時非常短;再標記階段STW時間很短,最多1ms刘绣,超過1ms則再次進入并發(fā)標記階段樱溉。即,ZGC幾乎所有暫停都只依賴于GC Roots集合大小额港,停頓時間不會隨著堆的大小或者活躍對象的大小而增加饺窿。與ZGC對比,G1的轉(zhuǎn)移階段完全STW的移斩,且停頓時間隨存活對象的大小增加而增加肚医。
ZGC關(guān)鍵技術(shù)
ZGC通過著色指針和讀屏障技術(shù),解決了轉(zhuǎn)移過程中準確訪問對象的問題向瓷,實現(xiàn)了并發(fā)轉(zhuǎn)移肠套。大致原理描述如下:并發(fā)轉(zhuǎn)移中“并發(fā)”意味著GC線程在轉(zhuǎn)移對象的過程中,應(yīng)用線程也在不停地訪問對象猖任。假設(shè)對象發(fā)生轉(zhuǎn)移你稚,但對象地址未及時更新,那么應(yīng)用線程可能訪問到舊地址朱躺,從而造成錯誤刁赖。而在ZGC中,應(yīng)用線程訪問對象將觸發(fā)“讀屏障”长搀,如果發(fā)現(xiàn)對象被移動了宇弛,那么“讀屏障”會把讀出來的指針更新到對象的新地址上,這樣應(yīng)用線程始終訪問的都是對象的新地址源请。那么枪芒,JVM是如何判斷對象被移動過呢?就是利用對象引用的地址谁尸,即著色指針试吁。下面介紹著色指針和讀屏障技術(shù)細節(jié)惕橙。
著色指針
**| **著色指針是一種將信息存儲在指針中的技術(shù)锐想。
ZGC僅支持64位系統(tǒng)然走,它把64位虛擬地址空間劃分為多個子空間,如下圖所示:
其中背镇,[0~4TB) 對應(yīng)Java堆咬展,[4TB ~ 8TB) 稱為M0地址空間,[8TB ~ 12TB) 稱為M1地址空間瞒斩,[12TB ~ 16TB) 預(yù)留未使用,[16TB ~ 20TB) 稱為Remapped空間涮总。
當(dāng)應(yīng)用程序創(chuàng)建對象時胸囱,首先在堆空間申請一個虛擬地址,但該虛擬地址并不會映射到真正的物理地址瀑梗。ZGC同時會為該對象在M0烹笔、M1和Remapped地址空間分別申請一個虛擬地址裳扯,且這三個虛擬地址對應(yīng)同一個物理地址,但這三個空間在同一時間有且只有一個空間有效谤职。ZGC之所以設(shè)置三個虛擬地址空間饰豺,是因為它使用“空間換時間”思想,去降低GC停頓時間允蜈≡┒郑“空間換時間”中的空間是虛擬空間,而不是真正的物理空間饶套。后續(xù)章節(jié)將詳細介紹這三個空間的切換過程漩蟆。
與上述地址空間劃分相對應(yīng),ZGC實際僅使用64位地址空間的第041位妓蛮,而第4245位存儲元數(shù)據(jù)怠李,第47~63位固定為0。
ZGC將對象存活信息存儲在42~45位中蛤克,這與傳統(tǒng)的垃圾回收并將對象存活信息放在對象頭中完全不同捺癞。
讀屏障
**| **讀屏障是JVM向應(yīng)用代碼插入一小段代碼的技術(shù)。當(dāng)應(yīng)用線程從堆中讀取對象引用時构挤,就會執(zhí)行這段代碼髓介。需要注意的是,僅“從堆中讀取對象引用”才會觸發(fā)這段代碼儿倒。
讀屏障示例:
Object o = obj.FieldA // 從堆中讀取引用版保,需要加入屏障
<Load barrier>
Object p = o // 無需加入屏障,因為不是從堆中讀取引用
o.dosomething() // 無需加入屏障夫否,因為不是從堆中讀取引用
int i = obj.FieldB //無需加入屏障彻犁,因為不是對象引用
ZGC中讀屏障的代碼作用:在對象標記和轉(zhuǎn)移過程中,用于確定對象的引用地址是否滿足條件凰慈,并作出相應(yīng)動作汞幢。
ZGC并發(fā)處理演示
接下來詳細介紹ZGC一次垃圾回收周期中地址視圖的切換過程:
- 初始化:ZGC初始化之后,整個內(nèi)存空間的地址視圖被設(shè)置為Remapped微谓。程序正常運行森篷,在內(nèi)存中分配對象,滿足一定條件后垃圾回收啟動豺型,此時進入標記階段仲智。
- 并發(fā)標記階段:第一次進入標記階段時視圖為M0,如果對象被GC標記線程或者應(yīng)用線程訪問過姻氨,那么就將對象的地址視圖從Remapped調(diào)整為M0钓辆。所以,在標記階段結(jié)束之后,對象的地址要么是M0視圖前联,要么是Remapped功戚。如果對象的地址是M0視圖,那么說明對象是活躍的似嗤;如果對象的地址是Remapped視圖啸臀,說明對象是不活躍的。
- 并發(fā)轉(zhuǎn)移階段:標記結(jié)束后就進入轉(zhuǎn)移階段烁落,此時地址視圖再次被設(shè)置為Remapped乘粒。如果對象被GC轉(zhuǎn)移線程或者應(yīng)用線程訪問過,那么就將對象的地址視圖從M0調(diào)整為Remapped顽馋。
其實谓厘,在標記階段存在兩個地址視圖M0和M1,上面的過程顯示只用了一個地址視圖寸谜。之所以設(shè)計成兩個竟稳,是為了區(qū)別前一次標記和當(dāng)前標記。即第二次進入并發(fā)標記階段后熊痴,地址視圖調(diào)整為M1他爸,而非M0。
著色指針和讀屏障技術(shù)不僅應(yīng)用在并發(fā)轉(zhuǎn)移階段果善,還應(yīng)用在并發(fā)標記階段:將對象設(shè)置為已標記诊笤,傳統(tǒng)的垃圾回收器需要進行一次內(nèi)存訪問,并將對象存活信息放在對象頭中巾陕;而在ZGC中讨跟,只需要設(shè)置指針地址的第42~45位即可,并且因為是寄存器訪問鄙煤,所以速度比訪問內(nèi)存更快晾匠。
ZGC調(diào)優(yōu)實踐
ZGC不是“銀彈”,需要根據(jù)服務(wù)的具體特點進行調(diào)優(yōu)梯刚。網(wǎng)絡(luò)上能搜索到實戰(zhàn)經(jīng)驗較少凉馆,調(diào)優(yōu)理論需自行摸索,我們在此階段也耗費了不少時間亡资,最終才達到理想的性能澜共。本文的一個目的是列舉一些使用ZGC時常見的問題,幫助大家使用ZGC提高服務(wù)可用性锥腻。
調(diào)優(yōu)基礎(chǔ)知識
理解ZGC重要配置參數(shù)
以我們服務(wù)在生產(chǎn)環(huán)境中ZGC參數(shù)配置為例嗦董,說明各個參數(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ù)因為有一定特殊性,所以設(shè)置的較大睡毒,后面會詳細介紹来惧。
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC:啟用ZGC的配置。
-XX:ConcGCThreads:并發(fā)回收垃圾的線程演顾。默認是總核數(shù)的12.5%供搀,8核CPU默認是1。調(diào)大后GC變快钠至,但會占用程序運行時的CPU資源葛虐,吞吐會受到影響。
-XX:ParallelGCThreads:STW階段使用線程數(shù)棉钧,默認是總核數(shù)的60%屿脐。
-XX:ZCollectionInterval:ZGC發(fā)生的最小時間間隔,單位秒宪卿。
-XX:ZAllocationSpikeTolerance:ZGC觸發(fā)自適應(yīng)算法的修正系數(shù)的诵,默認2,數(shù)值越大佑钾,越早的觸發(fā)ZGC西疤。
-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive:是否啟用主動回收,默認開啟休溶,這里的配置表示關(guān)閉代赁。
-Xlog:設(shè)置GC日志中的內(nèi)容、格式兽掰、位置以及每個日志的大小芭碍。
理解ZGC觸發(fā)時機
相比于CMS和G1的GC觸發(fā)機制,ZGC的GC觸發(fā)機制有很大不同禾进。ZGC的核心特點是并發(fā)豁跑,GC過程中一直有新的對象產(chǎn)生。如何保證在GC完成之前泻云,新產(chǎn)生的對象不會將堆占滿艇拍,是ZGC參數(shù)調(diào)優(yōu)的第一大目標。因為在ZGC中宠纯,當(dāng)垃圾來不及回收將堆占滿時卸夕,會導(dǎo)致正在運行的線程停頓,持續(xù)時間可能長達秒級之久婆瓜。
ZGC有多種GC觸發(fā)機制快集,總結(jié)如下:
- 阻塞內(nèi)存分配請求觸發(fā):當(dāng)垃圾來不及回收贡羔,垃圾將堆占滿時,會導(dǎo)致部分線程阻塞个初。我們應(yīng)當(dāng)避免出現(xiàn)這種觸發(fā)方式乖寒。日志中關(guān)鍵字是“Allocation Stall”。
- 基于分配速率的自適應(yīng)算法:最主要的GC觸發(fā)方式院溺,其算法原理可簡單描述為”ZGC根據(jù)近期的對象分配速率以及GC時間楣嘁,計算出當(dāng)內(nèi)存占用達到什么閾值時觸發(fā)下一次GC”。自適應(yīng)算法的詳細理論可參考彭成寒《新一代垃圾回收器ZGC設(shè)計與實現(xiàn)》一書中的內(nèi)容珍逸。通過ZAllocationSpikeTolerance參數(shù)控制閾值大小逐虚,該參數(shù)默認2,數(shù)值越大谆膳,越早的觸發(fā)GC叭爱。我們通過調(diào)整此參數(shù)解決了一些問題。日志中關(guān)鍵字是“Allocation Rate”漱病。
- 基于固定時間間隔:通過ZCollectionInterval控制买雾,適合應(yīng)對突增流量場景。流量平穩(wěn)變化時缨称,自適應(yīng)算法可能在堆使用率達到95%以上才觸發(fā)GC凝果。流量突增時,自適應(yīng)算法觸發(fā)的時機可能會過晚睦尽,導(dǎo)致部分線程阻塞器净。我們通過調(diào)整此參數(shù)解決流量突增場景的問題,比如定時活動当凡、秒殺等場景山害。日志中關(guān)鍵字是“Timer”。
- 主動觸發(fā)規(guī)則:類似于固定間隔規(guī)則沿量,但時間間隔不固定浪慌,是ZGC自行算出來的時機,我們的服務(wù)因為已經(jīng)加了基于固定時間間隔的觸發(fā)機制朴则,所以通過-ZProactive參數(shù)將該功能關(guān)閉权纤,以免GC頻繁,影響服務(wù)可用性乌妒。日志中關(guān)鍵字是“Proactive”汹想。
- 預(yù)熱規(guī)則:服務(wù)剛啟動時出現(xiàn),一般不需要關(guān)注撤蚊。日志中關(guān)鍵字是“Warmup”古掏。
- 外部觸發(fā):代碼中顯式調(diào)用System.gc()觸發(fā)。日志中關(guān)鍵字是“System.gc()”侦啸。
- 元數(shù)據(jù)分配觸發(fā):元數(shù)據(jù)區(qū)不足時導(dǎo)致槽唾,一般不需要關(guān)注丧枪。日志中關(guān)鍵字是“Metadata GC Threshold”。
理解ZGC日志
一次完整的GC過程庞萍,需要注意的點已在圖中標出拧烦。
注意:該日志過濾了進入安全點的信息。正常情況挂绰,在一次GC過程中還穿插著進入安全點的操作屎篱。
GC日志中每一行都注明了GC過程中的信息,關(guān)鍵信息如下:
Start:開始GC葵蒂,并標明的GC觸發(fā)的原因。上圖中觸發(fā)原因是自適應(yīng)算法重虑。
Phase-Pause Mark Start:初始標記践付,會STW。
Phase-Pause Mark End:再次標記缺厉,會STW永高。
Phase-Pause Relocate Start:初始轉(zhuǎn)移,會STW提针。
Heap信息:記錄了GC過程中Mark命爬、Relocate前后的堆大小變化狀況。High和Low記錄了其中的最大值和最小值辐脖,我們一般關(guān)注High中Used的值饲宛,如果達到100%,在GC過程中一定存在內(nèi)存分配不足的情況嗜价,需要調(diào)整GC的觸發(fā)時機艇抠,更早或者更快地進行GC。
GC信息統(tǒng)計:可以定時的打印垃圾收集信息久锥,觀察10秒內(nèi)家淤、10分鐘內(nèi)、10個小時內(nèi)瑟由,從啟動到現(xiàn)在的所有統(tǒng)計信息絮重。利用這些統(tǒng)計信息,可以排查定位一些異常點歹苦。
日志中內(nèi)容較多青伤,關(guān)鍵點已用紅線標出,含義較好理解暂氯,更詳細的解釋大家可以自行在網(wǎng)上查閱資料潮模。
理解ZGC停頓原因
我們在實戰(zhàn)過程中共發(fā)現(xiàn)了6種使程序停頓的場景,分別如下:
- GC時痴施,初始標記:日志中Pause Mark Start擎厢。
- GC時究流,再標記:日志中Pause Mark End。
- GC時动遭,初始轉(zhuǎn)移:日志中Pause Relocate Start芬探。
- 內(nèi)存分配阻塞:當(dāng)內(nèi)存不足時線程會阻塞等待GC完成,關(guān)鍵字是"Allocation Stall"厘惦。
- 安全點:所有線程進入到安全點后才能進行GC偷仿,ZGC定期進入安全點判斷是否需要GC。先進入安全點的線程需要等待后進入安全點的線程直到所有線程掛起宵蕉。
- dump線程酝静、內(nèi)存:比如jstack、jmap命令羡玛。
調(diào)優(yōu)案例
我們維護的服務(wù)名叫Zeus别智,它是美團的規(guī)則平臺,常用于風(fēng)控場景中的規(guī)則管理稼稿。規(guī)則運行是基于開源的表達式執(zhí)行引擎Aviator薄榛。Aviator內(nèi)部將每一條表達式轉(zhuǎn)化成Java的一個類,通過調(diào)用該類的接口實現(xiàn)表達式邏輯让歼。
Zeus服務(wù)內(nèi)的規(guī)則數(shù)量超過萬條敞恋,且每臺機器每天的請求量幾百萬。這些客觀條件導(dǎo)致Aviator生成的類和方法會產(chǎn)生很多的ClassLoader和CodeCache谋右,這些在使用ZGC時都成為過GC的性能瓶頸硬猫。接下來介紹兩類調(diào)優(yōu)案例。
內(nèi)存分配阻塞倚评,系統(tǒng)停頓可達到秒級
案例一:秒殺活動中流量突增浦徊,出現(xiàn)性能毛刺
日志信息:對比出現(xiàn)性能毛刺時間點的GC日志和業(yè)務(wù)日志,發(fā)現(xiàn)JVM停頓了較長時間天梧,且停頓時GC日志中有大量的“Allocation Stall”日志盔性。
分析:這種案例多出現(xiàn)在“自適應(yīng)算法”為主要GC觸發(fā)機制的場景中。ZGC是一款并發(fā)的垃圾回收器呢岗,GC線程和應(yīng)用線程同時活動冕香,在GC過程中,還會產(chǎn)生新的對象后豫。GC完成之前悉尾,新產(chǎn)生的對象將堆占滿,那么應(yīng)用線程可能因為申請內(nèi)存失敗而導(dǎo)致線程阻塞挫酿。當(dāng)秒殺活動開始构眯,大量請求打入系統(tǒng),但自適應(yīng)算法計算的GC觸發(fā)間隔較長早龟,導(dǎo)致GC觸發(fā)不及時惫霸,引起了內(nèi)存分配阻塞猫缭,導(dǎo)致停頓。
解決方法:
- 開啟”基于固定時間間隔“的GC觸發(fā)機制:-XX:ZCollectionInterval壹店。比如調(diào)整為5秒猜丹,甚至更短。
- 增大修正系數(shù)-XX:ZAllocationSpikeTolerance硅卢,更早觸發(fā)GC射窒。ZGC采用正態(tài)分布模型預(yù)測內(nèi)存分配速率,模型修正系數(shù)ZAllocationSpikeTolerance默認值為2将塑,值越大脉顿,越早的觸發(fā)GC,Zeus中所有集群設(shè)置的是5抬旺。
案例二:壓測時弊予,流量逐漸增大到一定程度后,出現(xiàn)性能毛刺
日志信息:平均1秒GC一次开财,兩次GC之間幾乎沒有間隔。
分析:GC觸發(fā)及時误褪,但內(nèi)存標記和回收速度過慢责鳍,引起內(nèi)存分配阻塞,導(dǎo)致停頓兽间。
解決方法:增大-XX:ConcGCThreads历葛,加快并發(fā)標記和回收速度。ConcGCThreads默認值是核數(shù)的1/8嘀略,8核機器恤溶,默認值是1。該參數(shù)影響系統(tǒng)吞吐帜羊,如果GC間隔時間大于GC周期咒程,不建議調(diào)整該參數(shù)。
GC Roots 數(shù)量大讼育,單次GC停頓時間長
案例三:單次GC停頓時間30ms帐姻,與預(yù)期停頓10ms左右有較大差距
日志信息:觀察ZGC日志信息統(tǒng)計,“Pause Roots ClassLoaderDataGraph”一項耗時較長奶段。
分析:dump內(nèi)存文件饥瓷,發(fā)現(xiàn)系統(tǒng)中有上萬個ClassLoader實例。我們知道ClassLoader屬于GC Roots一部分痹籍,且ZGC停頓時間與GC Roots成正比呢铆,GC Roots數(shù)量越大,停頓時間越久蹲缠。再進一步分析棺克,ClassLoader的類名表明悠垛,這些ClassLoader均由Aviator組件生成。分析Aviator源碼逆航,發(fā)現(xiàn)Aviator對每一個表達式新生成類時鼎文,會創(chuàng)建一個ClassLoader,這導(dǎo)致了ClassLoader數(shù)量巨大的問題因俐。在更高Aviator版本中拇惋,該問題已經(jīng)被修復(fù),即僅創(chuàng)建一個ClassLoader為所有表達式生成類抹剩。
解決方法:升級Aviator組件版本撑帖,避免生成多余的ClassLoader。
案例四:服務(wù)啟動后澳眷,運行時間越長胡嘿,單次GC時間越長,重啟后恢復(fù)
日志信息:觀察ZGC日志信息統(tǒng)計钳踊,“Pause Roots CodeCache”的耗時會隨著服務(wù)運行時間逐漸增長衷敌。
分析:CodeCache空間用于存放Java熱點代碼的JIT編譯結(jié)果,而CodeCache也屬于GC Roots一部分拓瞪。通過添加-XX:+PrintCodeCacheOnCompilation參數(shù)缴罗,打印CodeCache中的被優(yōu)化的方法,發(fā)現(xiàn)大量的Aviator表達式代碼祭埂。定位到根本原因面氓,每個表達式都是一個類中一個方法。隨著運行時間越長蛆橡,執(zhí)行次數(shù)增加舌界,這些方法會被JIT優(yōu)化編譯進入到Code Cache中,導(dǎo)致CodeCache越來越大泰演。
解決方法:JIT有一些參數(shù)配置可以調(diào)整JIT編譯的條件呻拌,但對于我們的問題都不太適用。我們最終通過業(yè)務(wù)優(yōu)化解決粥血,刪除不需要執(zhí)行的Aviator表達式柏锄,從而避免了大量Aviator方法進入CodeCache中。
值得一提的是复亏,我們并不是在所有這些問題都解決后才全量部署所有集群趾娃。即使開始有各種各樣的毛刺,但計算后發(fā)現(xiàn)缔御,有各種問題的ZGC也比之前的CMS對服務(wù)可用性影響小抬闷。所以從開始準備使用ZGC到全量部署,大概用了2周的時間。在之后的3個月時間里笤成,我們邊做業(yè)務(wù)需求评架,邊跟進這些問題,最終逐個解決了上述問題炕泳,從而使ZGC在各個集群上達到了一個更好表現(xiàn)纵诞。
升級ZGC效果
延遲降低
**| **TP(Top Percentile)是一項衡量系統(tǒng)延遲的指標:TP999表示99.9%請求都能被響應(yīng)的最小耗時;TP99表示99%請求都能被響應(yīng)的最小耗時培遵。
在Zeus服務(wù)不同集群中浙芙,ZGC在低延遲(TP999 < 200ms)場景中收益較大:
- TP999:下降12142ms,下降幅度18%74%籽腕。
- TP99:下降528ms嗡呼,下降幅度10%47%。
超低延遲(TP999 < 20ms)和高延遲(TP999 > 200ms)服務(wù)收益不大皇耗,原因是這些服務(wù)的響應(yīng)時間瓶頸不是GC南窗,而是外部依賴的性能。
吞吐下降
對吞吐量優(yōu)先的場景郎楼,ZGC可能并不適合万伤。例如,Zeus某離線集群原先使用CMS呜袁,升級ZGC后壕翩,系統(tǒng)吞吐量明顯降低。究其原因有二:第一傅寡,ZGC是單代垃圾回收器,而CMS是分代垃圾回收器北救。單代垃圾回收器每次處理的對象更多荐操,更耗費CPU資源;第二珍策,ZGC使用讀屏障托启,讀屏障操作需耗費額外的計算資源。
總結(jié)
ZGC作為下一代垃圾回收器攘宙,性能非常優(yōu)秀屯耸。ZGC垃圾回收過程幾乎全部是并發(fā),實際STW停頓時間極短蹭劈,不到10ms疗绣。這得益于其采用的著色指針和讀屏障技術(shù)。
Zeus在升級JDK 11+ZGC中铺韧,通過將風(fēng)險和問題分類多矮,然后各個擊破,最終順利實現(xiàn)了升級目標,GC停頓也幾乎不再影響系統(tǒng)可用性塔逃。
最后推薦大家升級ZGC讯壶,Zeus系統(tǒng)因為業(yè)務(wù)特點,遇到了較多問題湾盗,而風(fēng)控其他團隊在升級時都非常順利伏蚊。
參考文獻
- ZGC官網(wǎng)
- 彭成寒.《新一代垃圾回收器ZGC設(shè)計與實現(xiàn)》. 機械工業(yè)出版社, 2019.
- 從實際案例聊聊Java應(yīng)用的GC優(yōu)化
- Java Hotspot G1 GC的一些關(guān)鍵技術(shù)
附錄
如何使用新技術(shù)
在生產(chǎn)環(huán)境升級JDK 11,使用ZGC格粪,大家最關(guān)心的可能不是效果怎么樣躏吊,而是這個新版本用的人少,網(wǎng)上實踐也少匀借,靠不靠譜颜阐,穩(wěn)不穩(wěn)定。其次是升級成本會不會很大吓肋,萬一不成功豈不是白白浪費時間凳怨。所以,在使用新技術(shù)前是鬼,首先要做的是評估收益肤舞、成本和風(fēng)險。
評估收益
對于JDK這種世界關(guān)注的程序均蜜,大版本升級所引入的新技術(shù)一般已經(jīng)在理論上經(jīng)過驗證李剖。我們要做的事情就是確定當(dāng)前系統(tǒng)的瓶頸是否是新版本JDK可解決的問題,切忌問題未診斷清楚就采取措施囤耳。評估完收益之后再評估成本和風(fēng)險篙顺,收益過大或者過小,其他兩項影響權(quán)重就會小很多充择。
以本文開頭提到的案例為例德玫,假設(shè)GC次數(shù)不變(10次/分鐘),且單次GC時間從40ms降低10ms椎麦。通過計算宰僧,一分鐘內(nèi)有100/60000 = 0.17%的時間在進行GC,且期間所有請求僅停頓10ms观挎,GC期間影響的請求數(shù)和因GC增加的延遲都有所減少琴儿。
評估成本
這里主要指升級所需要的人力成本。此項相對比較成熟嘁捷,根據(jù)新技術(shù)的使用手冊判斷改動點造成。跟做其他項目區(qū)別不大,不再具體細說普气。
在我們的實踐中谜疤,兩周時間完成線上部署,達到安全穩(wěn)定運行的狀態(tài)。后續(xù)持續(xù)迭代3個月夷磕,根據(jù)業(yè)務(wù)場景對ZGC進行了更契合的優(yōu)化適配履肃。
評估風(fēng)險
升級JDK的風(fēng)險可以分為三類:
- 兼容性風(fēng)險:Java程序JAR包依賴很多,升級JDK版本后程序是否能運行起來坐桩。例如我們的服務(wù)是從JDK 7升級到JDK 11尺棋,需要解決較多JAR包不兼容的問題。
- 功能風(fēng)險:運行起來后绵跷,是否會有一些組件邏輯變更膘螟,影響現(xiàn)有功能的邏輯。
- 性能風(fēng)險:功能如果沒有問題碾局,性能是否穩(wěn)定荆残,能穩(wěn)定的在線上運行。
經(jīng)過分類后净当,每類風(fēng)險的應(yīng)對轉(zhuǎn)化成了常見的測試問題内斯,不再屬于未知風(fēng)險。風(fēng)險是指不確定的事情像啼,如果不確定的事情都能轉(zhuǎn)化成可確定的事情俘闯,意味著風(fēng)險已消除。
升級JDK 11
選擇JDK 11忽冻,是因為在JDK 11中首次支持ZGC真朗,而且JDK 11屬于長期支持(Long Term Support,LTS)版本僧诚,至少會被維護三年遮婶,普通版本(如JDK 12、JDK 13和JDK 14)只有6個月的維護周期湖笨,不建議使用蹭睡。
本地測試環(huán)境安裝
從兩個源OpenJDK和OracleJDK下載JDK 11,二個版本的JDK主要區(qū)別是長時期的免費和付費赶么,短期內(nèi)都免費。注意JDK 11版本中的ZGC不支持Mac OS系統(tǒng)脊串,在Mac OS系統(tǒng)上使用JDK 11只能用其他垃圾回收器辫呻,如G1。
生產(chǎn)環(huán)境安裝
升級JDK 11不僅僅是升級自己項目的JDK版本琼锋,還需要編譯放闺、發(fā)布部署、運行缕坎、監(jiān)控怖侦、性能內(nèi)存分析工具等項目支持。美團內(nèi)部的實踐:
編譯打包:美團發(fā)布系統(tǒng)支持選擇JDK 11進行編譯打包。
線上運行 & 全量部署:要求線上機器已安裝JDK 11匾寝,有3種方式:
- 新申請默認安裝JDK 11的虛擬機:試用JDK 11時可用這種方式搬葬;全量部署時,如果新申請機器數(shù)量過多艳悔,可能沒有足夠機器資源急凰。
- 通過手寫腳本給存量虛擬機安裝JDK 11:不推薦,業(yè)務(wù)同學(xué)過多參與到運維當(dāng)中猜年。
- 使用容器提供的鏡像部署功能抡锈,在打包鏡像時安裝JDK 11:推薦方式,不需要新申請資源乔外。
監(jiān)控指標:主要是GC的時間和頻率床三,我們通過美團的CAT監(jiān)控系統(tǒng)支持ZGC數(shù)據(jù)的收集(CAT已開源)。
性能內(nèi)存分析:線上遇到性能問題時杨幼,還需要借助Profiling工具撇簿,美團的性能診斷優(yōu)化平臺Scalpel已支持JDK 11的性能內(nèi)存分析。如果你的公司沒有相關(guān)工具推汽,推薦使用JProfier补疑。
解決組件兼容性
我們的項目包含二十多萬行代碼,需要從JDK 7升級到JDK 11歹撒,依賴組件眾多莲组。雖然看起來升級會比較復(fù)雜,但實際只花了兩天時間即解決了兼容性問題暖夭。具體過程如下:
1. 編譯锹杈,需要修改pom文件中的build配置,根據(jù)報錯作修改迈着,主要有兩類:
a. 一些類被刪除:比如“sun.misc.BASE64Encoder”竭望,找到替換類java.util.Base64即可。
b. 組件依賴版本不兼容JDK 11問題:找到對應(yīng)依賴組件裕菠,搜索最新版本咬清,一般都支持JDK 11。
2. 編譯成功后奴潘,啟動運行旧烧,此時仍有可能組件依賴版本問題,按照編譯時的方式處理即可画髓。
升級所修改的依賴:
<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)出來兩年掘剪,常見的依賴組件都有兼容性版本哲思。但是皿哨,如果是公司內(nèi)部提供的公司級組件筹燕,可能會不兼容JDK 11,需要推動相關(guān)組件進行升級芋浮。如果對方升級較為困難杈抢,可以考慮拆分功能京腥,將依賴這些組件的功能單獨部署蜜另,繼續(xù)使用低版本JDK。隨著JDK 11的卓越性能被大家悉知扫腺,相信會有更多團隊會用JDK 11解決GC問題岗照,使用者越多,各個組件升級的動力也會越大笆环。
驗證功能正確性
通過完備的單測攒至、集成和回歸測試,保證功能正確性躁劣。