天天看點

一次簡單的 JVM 調優,性能提升了15%

背景

最近對負責的項目進行了一次性能優化,其中包括對 JVM 參數的調整,算是進行了一次簡單的 JVM 調優,JVM 參數調整之後,服務的整體性能有 15% 左右的提升,還算不錯。

先介紹一下項目的基本情況:

項目是一個高 QPS 壓力的 web 服務,單機 QPS 一直維持在 1.5K 以上,由于舊機器的”拖累”,配置的堆大小是 8G,其中 young 區是 4G,垃圾回收器用的是 parNew + CMS。

舊狀

首先是檢視目前 GC 的情況,主要是使用 jstat 檢視 GC 的概況,再檢視 gc log,分析單次 gc 的詳細狀況。

使用 jstat -gcutil pid 1000 每隔一秒列印一次 gc 統計資訊。

一次簡單的 JVM 調優,性能提升了15%

可以看到,單次 gc 平均耗時是 60ms 左右,還算可以接受,但 YGC 非常頻繁,基本上每秒一次,有的時候還會一秒兩次,在一秒兩次的時候,服務對業務響應時長的壓力就會變得很大。

接着檢視 gc log,列印 gc log 需要在 JVM 啟動參數裡添加以下參數:

-XX:+PrintGCDateStamps:列印 gc 發生的時間戳。

-XX:+PrintTenuringDistribution:列印 gc 發生時的分代資訊。

-XX:+PrintGCApplicationStoppedTime:列印 gc 停頓時長

-XX:+PrintGCApplicationConcurrentTime:列印 gc 間隔的服務運作時長

-XX:+PrintGCDetails:列印 gc 詳情,包括 gc 前/記憶體等。

-Xloggc:../gclogs/gc.log.date:指定 gc log 的路徑

看到的 gc log 形如:

一次簡單的 JVM 調優,性能提升了15%

單次 GC 方面并不能直接看出問題,但可以看到 gc 前有很多次 18ms 左右的停頓。

分析和調整

YGC 頻繁

直接檢視 gc log 并不直覺,我們可以借用一些可視化工具來幫助我們分析, [gceasy](https://gceasy.io/) 是個挺不錯的網站,我們把 gc log 上傳上去後, gceasy 可以幫助我們生成各個次元的圖表幫助分析。

檢視 gceasy 生成的報告,發現我們服務的 gc 吞吐量是 95%,它指的是 JVM 運作業務代碼的時長占 JVM 總運作時長的比例,這個比例确實有些低了,運作 100 分鐘就有 5 分鐘在執行 gc。幸好這些 GC 中絕大多數都是 YGC,單次時長可控且分布平均,這使得我們服務還能平穩運作。

解決這個問題要麼是減少對象的建立,要麼就增大 young 區。前者不是一時半會兒都解決的,需要查找代碼裡可能有問題的點,分步優化。

而後者雖然改一下配置就行,但以我們對 GC 最直覺的印象來說,增大 young 區,YGC 的時長也會迅速增大。

其實這點不必太過擔心,我們知道 YGC 的耗時是由 GC 标記 + GC 複制 組成的,相對于 GC 複制,GC 标記是非常快的。而 young 區内大多數對象的生命周期都非常短,如果将 young 區增大一倍,GC 标記的時長會提升一倍,但到 GC 發生時被标記的對象大部分已經死亡, GC 複制的時長肯定不會提升一倍,是以我們可以放心增大 young 區大小。

由于低記憶體舊機器都被換掉了,我把堆大小調整到了 12G,young 區保留為 8G。

分代調整

除了 GC 太頻繁之外,GC 後各分代的平均大小也需要調整。

一次簡單的 JVM 調優,性能提升了15%

我們知道 GC 的提升機制,每次 GC 後,JVM 存活代數大于 MaxTenuringThreshold 的對象提升到老年代。當然,JVM 還有動态年齡計算的規則:按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了 survivor 區的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡門檻值,但看各代總的記憶體大小,是達不到 survivor 區的一半的。

一次簡單的 JVM 調優,性能提升了15%

是以這十五個分代内的對象會一直在兩個 survivor 區之間來回複制,再觀察各分代的平均大小,可以看到,四代以上的對象已經有一半都會保留到老年區了,是以可以将這些對象直接提升到老年代,以減少對象在兩個 survivor 區之間複制的性能開銷。

是以我把 MaxTenuringThreshold 的值調整為 4,将存活超過四代的對象直接提升到老年代。

偏向鎖停頓

還有一個問題是 gc log 裡有很多 18ms 左右的停頓,有時候連續有十多條,雖然每次停頓時長不長,但連續多次累積的時間也非常可觀。

1.8 之後 JVM 對鎖進行了優化,添加了偏向鎖的概念,避免了很多不必要的加鎖操作,但偏向鎖一旦遇到鎖競争,取消鎖需要進入 safe point,導緻 STW。

解決方式很簡單,JVM 啟動參數裡添加 -XX:-UseBiasedLocking 即可。

結果

調整完 JVM 參數後先是對服務進行壓測,發現性能确實有提升,也沒有發生嚴重的 GC 問題,之後再把調整好的配置放到線上機器進行灰階,同時收集 gc log,再次進行分析。

由于 young 區大小翻倍了,是以 YGC 的頻率減半了,GC 的吞量提升到了 97.75%。平均 GC 時長略有上升,從 60ms 左右提升到了 66ms,還是挺符合預期的。

由于 CMS 在進行 GC 時也會清理 young 區,CMS 的時長也受到了影響,CMS 的最終标記和并發清理階段耗時增加了,也比較正常。

另外我還統計了對業務的影響,之前因為 GC 導緻逾時的請求大大減少了。

小結

總之,這是一次挺成功的 GC 調整,讓我對 GC 有了更深的了解,但由于沒有深入到 old 區,之前學習到的 CMS 相關的知識還沒有複習到。

不過性能優化并不是一朝一夕的事,需要時刻關注問題,及時做出調整。