一次簡單的 JVM 調優,拿去寫到簡歷裡
給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~
大家好,我是大彬。
JVM調優一直是面試官很喜歡問的問題。週末在網上看到一篇JVM調優的文章,給大家分享一下。
背景
最近對負責的專案進行了一次效能優化,其中包括對 JVM 引數的調整,算是進行了一次簡單的 JVM 調優,JVM 引數調整之後,服務的整體效能有 5% 左右的提升,還算不錯。
先介紹一下專案的基本情況:
專案是一個高 QPS 壓力的 web 服務,單機 QPS 一直維持在 1.5K 以上,由於舊機器的”拖累”,配置的堆大小是 8G,其中 young 區是 4G,垃圾回收器用的是 parNew + CMS。
舊狀
首先是檢視當前 GC 的情況,主要是使用 jstat
檢視 GC 的概況,再檢視 gc log,分析單次 gc 的詳細狀況。
使用 jstat -gcutil pid 1000
每隔一秒列印一次 gc 統計資訊。
可以看到,單次 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 形如:
單次 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 後各分代的平均大小也需要調整。
我們知道 GC 的提升機制,每次 GC 後,JVM 存活代數大於 MaxTenuringThreshold
的物件提升到老年代。當然,JVM 還有動態年齡計算的規則:按照年齡從小到大對其所佔用的大小進行累積,當累積的某個年齡大小超過了 survivor 區的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡閾值,但看各代總的記憶體大小,是達不到 survivor 區的一半的。
所以這十五個分代內的物件會一直在兩個 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 相關的知識還沒有複習到。
不過效能優化並不是一朝一夕的事,需要時刻關注問題,及時做出調整。
本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構等核心知識點,歡迎star~