分享一次簡單的 JVM 調(diào)優(yōu),拿去寫在簡歷上

本文收錄于 www.cswiki.top
分享篇 JVM 調(diào)優(yōu)的文章,算是入門級別的,對于初學(xué)者來說應(yīng)該算是一個很好的了解調(diào)優(yōu)思路的文章(對于校招就問有無 JVM 調(diào)優(yōu)經(jīng)驗的我只能說一句 sb),原文來自 https://zhenbianshu.github.io,里面有一些句子和圖片等我都重新做了整理和解釋
背景
最近對負(fù)責(zé)的項目進(jìn)行了一次性能優(yōu)化,其中包括對 JVM 參數(shù)的調(diào)整,算是進(jìn)行了一次簡單的 JVM 調(diào)優(yōu),JVM 參數(shù)調(diào)整之后,服務(wù)的整體性能有 5% 左右的提升,還算不錯。
先介紹一下項目的基本情況:
項目是一個高 QPS(Queries Per Second:每秒查詢率,一臺服務(wù)器每秒能夠響應(yīng)的查詢請求的次數(shù)) 壓力的 web 服務(wù),單機 QPS 一直維持在 1.5K 以上,由于舊機器的”拖累”,配置的堆大小是 8G,其中 Young 區(qū)是 4G,垃圾回收器用的是 parNew + CMS
舊狀
首先是查看當(dāng)前 GC 的情況,主要是使用 jstat 查看 GC 的概況,再查看 GC log,分析單次 GC 的詳細(xì)狀況。
使用 jstat -GCutil pid 1000 每隔一秒打印一次 GC 統(tǒng)計信息。

參數(shù)說明如下:
S0:新生代中 Survivor space 0 區(qū)已使用空間的百分比 S1:新生代中 Survivor space 1 區(qū)已使用空間的百分比 E:新生代已使用空間的百分比 O:老年代已使用空間的百分比 P:永久帶已使用空間的百分比 YGC:從應(yīng)用程序啟動到當(dāng)前,發(fā)生 Young GC 的次數(shù) YGCT:從應(yīng)用程序啟動到當(dāng)前,Young GC 所用的時間【單位秒】 FGC:從應(yīng)用程序啟動到當(dāng)前,發(fā)生 Full GC 的次數(shù) FGCT:從應(yīng)用程序啟動到當(dāng)前,F(xiàn)ull GC 所用的時間 GCT:從應(yīng)用程序啟動到當(dāng)前,用于垃圾回收的總時間【單位秒】
可以看到,單次 GC 平均耗時是 GCT / (YGC + FGC) = 60ms 左右 ,還算可以接受,但 YGC 太過頻繁。
接著查看 GC log,打印 GC log 需要在 JVM 啟動參數(shù)里添加以下參數(shù):
-XX:+PrintGCDateStamps:打印 GC 發(fā)生的時間戳。-XX:+PrintTenuringDistribution:打印 GC 發(fā)生時的分代信息。-XX:+PrintGCApplicationStoppedTime:打印 GC 停頓時長-XX:+PrintGCApplicationConcurrentTime:打印 GC 間隔的服務(wù)運行時長-XX:+PrintGCDetails:打印 GC 詳情,包括 GC 前/內(nèi)存等。-XlogGC:../GClogs/GC.log.date:指定 GC log 的路徑
看到的 GC log 形如:

單次 GC 方面并不能直接看出問題,但可以看到 GC 前有很多次約 18ms 左右的停頓。
分析和調(diào)整
YGC 頻繁
直接查看 GC log 并不直觀,我們可以借用一些可視化工具來幫助我們分析,GCeasy - https://GCeasy.io/ 是個挺不錯的網(wǎng)站,我們把 GC log 上傳上去后, GCeasy 可以幫助我們生成各個維度的圖表幫助分析。
查看 GCeasy 生成的報告,發(fā)現(xiàn)我們服務(wù)的 GC 吞吐量是 95% (GC 吞吐量指 GC 所花費的時間和系統(tǒng)總運行時間的比值, 系統(tǒng)總運行時間= 應(yīng)用程序耗時+GC 耗時),它指的是 JVM 運行業(yè)務(wù)代碼的時長占 JVM 總運行時長的比例,這個比例確實有些低了,運行 100 分鐘就有 5 分鐘在執(zhí)行 GC。幸好這些 GC 中絕大多數(shù)都是 YGC,單次時長可控且分布平均,這使得我們服務(wù)還能平穩(wěn)運行。
解決這個問題要么是減少對象的創(chuàng)建,要么就增大 Young 區(qū)。前者不是一時半會兒都解決的,需要查找代碼里可能有問題的點,分步優(yōu)化。
而后者雖然改一下配置就行,但以我們對 GC 最直觀的印象來說,增大 Young 區(qū),YGC 的時長也會迅速增大。
其實這點不必太過擔(dān)心,我們知道 YGC 的耗時是由 標(biāo)記 + 復(fù)制 組成的,相對于復(fù)制,標(biāo)記過程是非常快的。而 Young 區(qū)內(nèi)大多數(shù)對象的生命周期都非常短,如果將 Young 區(qū)增大一倍,標(biāo)記的時長會提升一倍,但到 GC 發(fā)生時被標(biāo)記的對象大部分已經(jīng)死亡, 復(fù)制的時長肯定不會提升一倍,所以我們可以放心增大 Young 區(qū)大小。
由于低內(nèi)存舊機器都被換掉了,我把堆大小從 8 調(diào)整到了 12G,Young 區(qū)從 4 提升為 8G。
分代調(diào)整
除了 GC 太頻繁之外,GC 后各分代的平均大小也需要調(diào)整。

我們知道 GC 的提升機制,每次 GC 后,JVM 存活代數(shù)大于 MaxTenuringThreshold 的對象提升到老年代。
當(dāng)然,JVM 還有動態(tài)年齡計算的規(guī)則:按照年齡從小到大對其所占用的大小進(jìn)行累積,當(dāng)累積的某個年齡大小超過了 survivor 區(qū)的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡閾值。不過看各代總的內(nèi)存大小,是達(dá)不到 survivor 區(qū)的一半的。

所以這十五個分代內(nèi)的對象會一直在兩個 Survivor 區(qū)之間來回復(fù)制,再觀察各分代的平均大小,可以看到,四代以上的對象已經(jīng)有一半都會保留到老年區(qū)了,所以可以將這些對象直接提升到老年代,以減少對象在兩個 survivor 區(qū)之間復(fù)制的性能開銷。
所以我把 MaxTenuringThreshold 的值調(diào)整為 4,將存活超過四代的對象直接提升到老年代。
偏向鎖停頓
還有一個問題是 GC log 里有很多約 18ms 左右的停頓,有時候連續(xù)有十多條,雖然每次停頓時長不長,但連續(xù)多次累積的時間也非常可觀。
這個問題其實就是 JDK1.8 之后 JVM 對鎖進(jìn)行了優(yōu)化,添加了偏向鎖的概念,避免了很多不必要的加鎖操作,但偏向鎖一旦遇到鎖競爭,就會進(jìn)行鎖釋放,而鎖釋放操作需要進(jìn)入安全點 safe point,導(dǎo)致 STW。
解決方式很簡單,JVM 啟動參數(shù)里添加 -XX:-UseBiasedLocking 取消偏向鎖即可 (JDK15 已經(jīng)廢棄偏向鎖了)。
結(jié)果
調(diào)整完 JVM 參數(shù)后先是對服務(wù)進(jìn)行壓測,發(fā)現(xiàn)性能確實有提升,也沒有發(fā)生嚴(yán)重的 GC 問題,之后再把調(diào)整好的配置放到線上機器進(jìn)行灰度,同時收集 GC log,再次進(jìn)行分析。
由于 Young 區(qū)大小翻倍了,所以 YGC 的頻率減半了,GC 的吞量提升到了 97.75%。不過平均 GC 時長略有上升,從 60ms 左右提升到了 66ms,還是挺符合預(yù)期的。
由于 CMS 在進(jìn)行 GC 時也會清理 Young 區(qū),CMS 的時長也受到了影響,CMS 的最終標(biāo)記和并發(fā)清理階段耗時增加了,也比較正常。
另外我還統(tǒng)計了對業(yè)務(wù)的影響,之前因為 GC 導(dǎo)致超時的請求大大減少了。
心之所向,素履以往,我是小牛肉,回復(fù)『春秋招』我拉你進(jìn)春秋招交流群
