互聯(lián)網(wǎng)/程序員/技術/資料共享
來自:https://zhenbianshu.github.io
前不久剛為一個項目調(diào)優(yōu)了 GC ,還沒來得及整理,今天在網(wǎng)上看到一篇調(diào)優(yōu)文,先來分享一下。來源地址:https://zhenbianshu.github.io背景
最近對負責的項目進行了一次性能優(yōu)化,其中包括對 JVM 參數(shù)的調(diào)整,算是進行了一次簡單的 JVM 調(diào)優(yōu),JVM 參數(shù)調(diào)整之后,服務的整體性能有 5% 左右的提升,還算不錯。項目是一個高 QPS 壓力的 web 服務,單機 QPS 一直維持在 1.5K 以上,由于舊機器的”拖累”,配置的堆大小是 8G,其中 young 區(qū)是 4G,垃圾回收器用的是 parNew + CMS。舊狀
首先是查看當前 GC 的情況,主要是使用 jstat 查看 GC 的概況,再查看 gc log,分析單次 gc 的詳細狀況。使用 jstat -gcutil pid 1000 每隔一秒打印一次 gc 統(tǒng)計信息。可以看到,單次 gc 平均耗時是 60ms 左右,還算可以接受,但 YGC 非常頻繁,基本上每秒一次,有的時候還會一秒兩次,在一秒兩次的時候,服務對業(yè)務響應時長的壓力就會變得很大。接著查看 gc log,打印 gc log 需要在 JVM 啟動參數(shù)里添加以下參數(shù):-XX:+PrintGCDateStamps:打印 gc 發(fā)生的時間戳。-XX:+PrintTenuringDistribution:打印 gc 發(fā)生時的分代信息。-XX:+PrintGCApplicationStoppedTime:打印 gc 停頓時長-XX:+PrintGCApplicationConcurrentTime:打印 gc 間隔的服務運行時長-XX:+PrintGCDetails:打印 gc 詳情,包括 gc 前/內(nèi)存等。-Xloggc:../gclogs/gc.log.date:指定 gc log 的路徑
單次 GC 方面并不能直接看出問題,但可以看到 gc 前有很多次 18ms 左右的停頓。分析和調(diào)整
YGC 頻繁
直接查看 gc log 并不直觀,我們可以借用一些可視化工具來幫助我們分析, [gceasy](https://gceasy.io/) 是個挺不錯的網(wǎng)站,我們把 gc log 上傳上去后, gceasy 可以幫助我們生成各個維度的圖表幫助分析。查看 gceasy 生成的報告,發(fā)現(xiàn)我們服務的 gc 吞吐量是 95%,它指的是 JVM 運行業(yè)務代碼的時長占 JVM 總運行時長的比例,這個比例確實有些低了,運行 100 分鐘就有 5 分鐘在執(zhí)行 gc。幸好這些 GC 中絕大多數(shù)都是 YGC,單次時長可控且分布平均,這使得我們服務還能平穩(wěn)運行。解決這個問題要么是減少對象的創(chuàng)建,要么就增大 young 區(qū)。前者不是一時半會兒都解決的,需要查找代碼里可能有問題的點,分步優(yōu)化。而后者雖然改一下配置就行,但以我們對 GC 最直觀的印象來說,增大 young 區(qū),YGC 的時長也會迅速增大。其實這點不必太過擔心,我們知道 YGC 的耗時是由 GC 標記 + GC 復制 組成的,相對于 GC 復制,GC 標記是非常快的。而 young 區(qū)內(nèi)大多數(shù)對象的生命周期都非常短,如果將 young 區(qū)增大一倍,GC 標記的時長會提升一倍,但到 GC 發(fā)生時被標記的對象大部分已經(jīng)死亡, GC 復制的時長肯定不會提升一倍,所以我們可以放心增大 young 區(qū)大小。由于低內(nèi)存舊機器都被換掉了,我把堆大小調(diào)整到了 12G,young 區(qū)保留為 8G。分代調(diào)整
除了 GC 太頻繁之外,GC 后各分代的平均大小也需要調(diào)整。我們知道 GC 的提升機制,每次 GC 后,JVM 存活代數(shù)大于 MaxTenuringThreshold 的對象提升到老年代。當然,JVM 還有動態(tài)年齡計算的規(guī)則:按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了 survivor 區(qū)的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡閾值,但看各代總的內(nèi)存大小,是達不到 survivor 區(qū)的一半的。所以這十五個分代內(nèi)的對象會一直在兩個 survivor 區(qū)之間來回復制,再觀察各分代的平均大小,可以看到,四代以上的對象已經(jīng)有一半都會保留到老年區(qū)了,所以可以將這些對象直接提升到老年代,以減少對象在兩個 survivor 區(qū)之間復制的性能開銷。所以我把 MaxTenuringThreshold 的值調(diào)整為 4,將存活超過四代的對象直接提升到老年代。偏向鎖停頓
還有一個問題是 gc log 里有很多 18ms 左右的停頓,有時候連續(xù)有十多條,雖然每次停頓時長不長,但連續(xù)多次累積的時間也非??捎^。是因為 1.8 之后 JVM 對鎖進行了優(yōu)化,添加了偏向鎖的概念,避免了很多不必要的加鎖操作,但偏向鎖一旦遇到鎖競爭,取消鎖需要進入 safe point,導致 STW。解決方式很簡單,JVM 啟動參數(shù)里添加 -XX:-UseBiasedLocking 即可。結果
調(diào)整完 JVM 參數(shù)后先是對服務進行壓測,發(fā)現(xiàn)性能確實有提升,也沒有發(fā)生嚴重的 GC 問題,之后再把調(diào)整好的配置放到線上機器進行灰度,同時收集 gc log,再次進行分析。由于 young 區(qū)大小翻倍了,所以 YGC 的頻率減半了,GC 的吞量提升到了 97.75%。平均 GC 時長略有上升,從 60ms 左右提升到了 66ms,還是挺符合預期的。由于 CMS 在進行 GC 時也會清理 young 區(qū),CMS 的時長也受到了影響,CMS 的最終標記和并發(fā)清理階段耗時增加了,也比較正常。另外我還統(tǒng)計了對業(yè)務的影響,之前因為 GC 導致超時的請求大大減少了。小結
總之,這是一次挺成功的 GC 調(diào)整,讓我對 GC 有了更深的理解,但由于沒有深入到 old 區(qū),之前學習到的 CMS 相關的知識還沒有復習到。不過性能優(yōu)化并不是一朝一夕的事,需要時刻關注問題,及時做出調(diào)整。推薦閱讀:
1行代碼生成隨機迷宮,這個概率編程語言登GitHub熱榜,作者曾開發(fā)著名WFC算法
Java批量更新太慢?多線程+List分段完美解決!
互聯(lián)網(wǎng)初中高級大廠面試題(9個G)內(nèi)容包含Java基礎、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務、Zookeeper......等技術棧!
?戳閱讀原文領??! 朕已閱 