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

來源:https://zhenbianshu.github.io/java

背景

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

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

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

舊狀

首先是查看當前 GC 的狀況,主要是使用 jstat 查看 GC 的概況,再查看 gc log,分析單次 gc 的詳細情況。面試

使用 jstat -gcutil pid 1000 每隔一秒打印一次 gc 統計信息。spring

能夠看到,單次 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 形如:intellij-idea

單次 GC 方面並不能直接看出問題,但能夠看到 gc 前有不少次 18ms 左右的停頓。ide

分析和調整

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 相關的知識尚未複習到。

不過性能優化並非一朝一夕的事,須要時刻關注問題,及時作出調整。
近期熱文推薦:

1.600+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上全部 Mock 工具!

4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

以爲不錯,別忘了隨手點贊+轉發哦!

相關文章
相關標籤/搜索