【JVM進階之路】十:JVM調優總結

三分惡發表於2021-04-11

1、調優原則

JVM調優聽起來很高大上,但是要認識到,JVM調優應該是Java效能優化的最後一顆子彈。

Java專案需要調優嗎

比較認可廖雪峰老師的觀點,要認識到JVM調優不是常規手段,效能問題一般第一選擇是優化程式,最後的選擇才是進行JVM調優。

調優層級

JVM的自動記憶體管理本來就是為了將開發人員從記憶體管理的泥潭裡拉出來。即使不得不進行JVM調優,也絕對不能拍腦門就去調整引數,一定要全面監控,詳細分析效能資料。

2、JVM調優的時機

不得不考慮進行JVM調優的是那些情況呢?

  • Heap記憶體(老年代)持續上漲達到設定的最大記憶體值;
  • Full GC 次數頻繁;
  • GC 停頓時間過長(超過1秒);
  • 應用出現OutOfMemory 等記憶體異常;
  • 應用中有使用本地快取且佔用大量記憶體空間;
  • 系統吞吐量與響應效能不高或下降。

3、JVM調優的目標

吞吐量、延遲、記憶體佔用三者類似CAP,構成了一個不可能三角,只能選擇其中兩個進行調優,不可三者兼得。

  • 延遲:GC低停頓和GC低頻率;
  • 低記憶體佔用;
  • 高吞吐量;

選擇了其中兩個,必然會會以犧牲另一個為代價。

下面展示了一些JVM調優的量化目標參考例項:

  • Heap 記憶體使用率 <= 70%;
  • Old generation記憶體使用率<= 70%;
  • avgpause <= 1秒;
  • Full gc 次數0 或 avg pause interval >= 24小時 ;

注意:不同應用的JVM調優量化目標是不一樣的。

4、JVM調優的步驟

一般情況下,JVM調優可通過以下步驟進行:

  • 分析系統系統執行情況:分析GC日誌及dump檔案,判斷是否需要優化,確定瓶頸問題點;
  • 確定JVM調優量化目標;
  • 確定JVM調優引數(根據歷史JVM引數來調整);
  • 依次確定調優記憶體、延遲、吞吐量等指標;
  • 對比觀察調優前後的差異;
  • 不斷的分析和調整,直到找到合適的JVM引數配置;
  • 找到最合適的引數,將這些引數應用到所有伺服器,並進行後續跟蹤。

以上操作步驟中,某些步驟是需要多次不斷迭代完成的。一般是從滿足程式的記憶體使用需求開始的,之後是時間延遲的要求,最後才是吞吐量的要求,要基於這個步驟來不斷優化,每一個步驟都是進行下一步的基礎,不可逆行。

JVM調優步驟

5、JVM引數

下面來看一下JDK的JVM引數。

5.1、基本引數

引數名稱 含義 預設值
-Xms 初始堆大小 記憶體的1/64 預設(MinHeapFreeRatio引數可以調整)空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 記憶體的1/4 預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到 -Xms的最小限制
-Xmn 年輕代大小 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。 整個堆大小=年輕代大小 + 年老代大小 + 持久代大小. 增大年輕代後,將會減小年老代大小.此值對系統效能影響較大,Sun官方推薦配置為整個堆的3/8
-XX:NewSize 設定年輕代大小
-XX:MaxNewSize 年輕代最大值
-XX:PermSize 設定持久代(perm gen)初始值 記憶體的1/64 JDK1.8以前
-XX:MaxPermSize 設定持久代最大值 記憶體的1/4 JDK1.8以前
-Xss 每個執行緒的堆疊大小 JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K.更具應用的執行緒所需記憶體大小進行 調整.在相同實體記憶體下,減小這個值能生成更多的執行緒.但是作業系統對一個程式內的執行緒數還是有限制的,不能無限生成,經驗值在3000~5000左右 一般小的應用, 如果棧不是很深, 應該是128k夠用的 大的應用建議使用256k。這個選項對效能影響比較大,需要嚴格的測試。(校長) 和threadstacksize選項解釋很類似,官方文件似乎沒有解釋,在論壇中有這樣一句話:"” -Xss is translated in a VM flag named ThreadStackSize” 一般設定這個值就可以了。
-XX:ThreadStackSize Thread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]
-XX:NewRatio 年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代) -XX:NewRatio=4表示年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5 Xms=Xmx並且設定了Xmn的情況下,該引數不需要進行設定。
-XX:SurvivorRatio Eden區與Survivor區的大小比值 設定為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區佔整個年輕代的1/10
-XX:LargePageSizeInBytes 記憶體頁的大小不可設定過大, 會影響Perm的大小 =128m
-XX:+UseFastAccessorMethods 原始型別的快速優化
-XX:+DisableExplicitGC 關閉System.gc() 這個引數需要嚴格的測試
-XX:+ExplicitGCInvokesConcurrent 關閉System.gc() disabled Enables invoking of concurrent GC by using the System.gc() request. This option is disabled by default and can be enabled only together with the -XX:+UseConcMarkSweepGC option.
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 關閉System.gc() disabled Enables invoking of concurrent GC by using the System.gc() request and unloading of classes during the concurrent GC cycle. This option is disabled by default and can be enabled only together with the -XX:+UseConcMarkSweepGC option.
-XX:MaxTenuringThreshold 垃圾最大年齡 如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代. 對於年老代比較多的應用,可以提高效率.如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件再年輕代的存活 時間,增加在年輕代即被回收的概率 該引數只有在序列GC時才有效.
-XX:+AggressiveOpts 加快編譯
-XX:+UseBiasedLocking 鎖機制的效能改善
-Xnoclassgc 禁用垃圾回收
-XX:SoftRefLRUPolicyMSPerMB 每兆堆空閒空間中SoftReference的存活時間 1s softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap
-XX:PretenureSizeThreshold 物件超過多大是直接在舊生代分配 0 單位位元組 新生代採用Parallel Scavenge GC時無效 另一種直接在舊生代分配的情況是大的陣列物件,且陣列中無外部引用物件.
-XX:TLABWasteTargetPercent TLAB佔eden區的百分比 1%
-XX:+CollectGen0First FullGC時是否先YGC false

Jdk7版本的主要引數

引數名稱 含義 預設值
-XX:PermSize 設定持久代 Jdk7版本及以前版本
-XX:MaxPermSize 設定最大持久代 Jdk7版本及以前版本

Jdk8版本的重要特有引數

引數名稱 含義 預設值
-XX:MetaspaceSize 元空間大小 Jdk8版本
-XX:MaxMetaspaceSize 最大元空間 Jdk8版本

5.2、並行收集器相關引數

引數名稱 含義 預設值
-XX:+UseParallelGC Full GC採用parallel MSC (此項待驗證) 選擇垃圾收集器為並行收集器.此配置僅對年輕代有效.即上述配置下,年輕代使用併發收集,而年老代仍舊使用序列收集.(此項待驗證)
-XX:+UseParNewGC 設定年輕代為並行收集 可與CMS收集同時使用 JDK5.0以上,JVM會根據系統配置自行設定,所以無需再設定此值
-XX:ParallelGCThreads 並行收集器的執行緒數 此值最好配置與處理器數目相等 同樣適用於CMS
-XX:+UseParallelOldGC 年老代垃圾收集方式為並行收集(Parallel Compacting) 這個是JAVA 6出現的引數選項
-XX:MaxGCPauseMillis 每次年輕代垃圾回收的最長時間(最大暫停時間) 如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值.
-XX:+UseAdaptiveSizePolicy 自動選擇年輕代區大小和相應的Survivor區比例 設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟.
-XX:GCTimeRatio 設定垃圾回收時間佔程式執行時間的百分比 公式為1/(1+n)
-XX:+ScavengeBeforeFullGC Full GC前呼叫YGC true Do young generation GC prior to a full GC. (Introduced in 1.4.1.)

5.3、CMS相關引數

引數名稱 含義 預設值
-XX:+UseConcMarkSweepGC 使用CMS記憶體收集 測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明.所以,此時年輕代大小最好用-Xmn設定.???
-XX:+AggressiveHeap 試圖是使用大量的實體記憶體 長時間大記憶體使用的優化,能檢查計算資源(記憶體, 處理器數量) 至少需要256MB記憶體 大量的CPU/記憶體, (在1.4.1在4CPU的機器上已經顯示有提升)
-XX:CMSFullGCsBeforeCompaction 多少次後進行記憶體壓縮 由於併發收集器不對記憶體空間進行壓縮,整理,所以執行一段時間以後會產生"碎片",使得執行效率降低.此值設定執行多少次GC以後對記憶體空間進行壓縮,整理.
-XX:+CMSParallelRemarkEnabled 降低標記停頓
-XX+UseCMSCompactAtFullCollection 在FULL GC的時候, 對年老代的壓縮 CMS是不會移動記憶體的, 因此, 這個非常容易產生碎片, 導致記憶體不夠用, 因此, 記憶體的壓縮這個時候就會被啟用。 增加這個引數是個好習慣。 可能會影響效能,但是可以消除碎片
-XX:+UseCMSInitiatingOccupancyOnly 使用手動定義初始化定義開始CMS收集 禁止hostspot自行觸發CMS GC
-XX:CMSInitiatingOccupancyFraction=70 使用cms作為垃圾回收 使用70%後開始CMS收集 92 為了保證不出現promotion failed(見下面介紹)錯誤,該值的設定需要滿足以下公式CMSInitiatingOccupancyFraction計算公式
-XX:CMSInitiatingPermOccupancyFraction 設定Perm Gen使用到達多少比率時觸發 92
-XX:+CMSIncrementalMode 設定為增量模式 用於單CPU情況
-XX:+CMSClassUnloadingEnabled

5.4、輔助資訊

引數名稱 含義 預設值
-XX:+PrintGC 輸出形式: [GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails 輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
-XX:+PrintGCTimeStamps
-XX:+PrintGC:PrintGCTimeStamps 可與-XX:+PrintGC -XX:+PrintGCDetails混合使用 輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationStoppedTime 列印垃圾回收期間程式暫停的時間.可與上面混合使用 輸出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:+PrintGCApplicationConcurrentTime 列印每次垃圾回收前,程式未中斷的執行時間.可與上面混合使用 輸出形式:Application time: 0.5291524 seconds
-XX:+PrintHeapAtGC 列印GC前後的詳細堆疊資訊
-Xloggc:filename 把相關日誌資訊記錄到檔案以便分析. 與上面幾個配合使用
-XX:+PrintClassHistogram garbage collects before printing the histogram.
-XX:+PrintTLAB 檢視TLAB空間的使用情況
XX:+PrintTenuringDistribution 檢視每次minor GC後新的存活週期的閾值 Desired survivor size 1048576 bytes, new threshold 7 (max 15) new threshold 7即標識新的存活週期的閾值為7。

6、主要工具

6.1、JDK工具

JDK自帶了很多效能監控工具,我們可以用這些工具來監測系統和排查記憶體效能問題。

JDK自帶工具

6.2、Linux 命令列工具

進行效能監控和問題排查的時候,常常是結合作業系統本身的命令列工具來進行。

命令 說明
top 實時顯示正在執行程式的 CPU 使用率、記憶體使用率以及系統負載等資訊
vmstat 對作業系統的虛擬記憶體、程式、CPU活動進行監控
pidstat 監控指定程式的上下文切換
iostat 監控磁碟IO

其它還有一些第三方的監控工具,同樣是效能分析和故障排查的利器,如MATGChistoJProfilerarthas

7、常用調優策略

這裡還是要提一下,及時確定要進行JVM調優,也不要陷入“知見障”,進行分析之後,發現可以通過優化程式提升效能,仍然首選優化程式。

7.1、選擇合適的垃圾回收器

CPU單核,那麼毫無疑問Serial 垃圾收集器是你唯一的選擇。

CPU多核,關注吞吐量 ,那麼選擇PS+PO組合。

CPU多核,關注使用者停頓時間,JDK版本1.6或者1.7,那麼選擇CMS。

CPU多核,關注使用者停頓時間,JDK1.8及以上,JVM可用記憶體6G以上,那麼選擇G1。

引數配置:

 //設定Serial垃圾收集器(新生代)
 開啟:-XX:+UseSerialGC
 ​
 //設定PS+PO,新生代使用功能Parallel Scavenge 老年代將會使用Parallel Old收集器
 開啟 -XX:+UseParallelOldGC
 ​
 //CMS垃圾收集器(老年代)
 開啟 -XX:+UseConcMarkSweepGC
 ​
 //設定G1垃圾收集器
 開啟 -XX:+UseG1GC

7.2、調整記憶體大小

現象:垃圾收集頻率非常頻繁。

原因:如果記憶體太小,就會導致頻繁的需要進行垃圾收集才能釋放出足夠的空間來建立新的物件,所以增加堆記憶體大小的效果是非常顯而易見的。

注意:如果垃圾收集次數非常頻繁,但是每次能回收的物件非常少,那麼這個時候並非記憶體太小,而可能是記憶體洩露導致物件無法回收,從而造成頻繁GC。

引數配置:

 //設定堆初始值
 指令1:-Xms2g
 指令2:-XX:InitialHeapSize=2048m
 ​
 //設定堆區最大值
 指令1:`-Xmx2g` 
 指令2: -XX:MaxHeapSize=2048m
 ​
 //新生代記憶體配置
 指令1:-Xmn512m
 指令2:-XX:MaxNewSize=512m

7.3、設定符合預期的停頓時間

現象:程式間接性的卡頓

原因:如果沒有確切的停頓時間設定,垃圾收集器以吞吐量為主,那麼垃圾收集時間就會不穩定。

注意:不要設定不切實際的停頓時間,單次時間越短也意味著需要更多的GC次數才能回收完原有數量的垃圾.

引數配置:

 //GC停頓時間,垃圾收集器會嘗試用各種手段達到這個時間
 -XX:MaxGCPauseMillis 

7.4、調整記憶體區域大小比率

現象:某一個區域的GC頻繁,其他都正常。

原因:如果對應區域空間不足,導致需要頻繁GC來釋放空間,在JVM堆記憶體無法增加的情況下,可以調整對應區域的大小比率。

注意:也許並非空間不足,而是因為記憶體洩造成記憶體無法回收。從而導致GC頻繁。

引數配置:

 //survivor區和Eden區大小比率
 指令:-XX:SurvivorRatio=6  //S區和Eden區佔新生代比率為1:6,兩個S區2:6
 ​
 //新生代和老年代的佔比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代佔整個堆的4/5;預設值=2

7.5、調整物件升老年代的年齡

現象:老年代頻繁GC,每次回收的物件很多。

原因:如果升代年齡小,新生代的物件很快就進入老年代了,導致老年代物件變多,而這些物件其實在隨後的很短時間內就可以回收,這時候可以調整物件的升級代年齡,讓物件不那麼容易進入老年代解決老年代空間不足頻繁GC問題。

注意:增加了年齡之後,這些物件在新生代的時間會變長可能導致新生代的GC頻率增加,並且頻繁複制這些物件新生的GC時間也可能變長。

配置引數:

//進入老年代最小的GC年齡,年輕代物件轉換為老年代物件最小年齡值,預設值7
 -XX:InitialTenuringThreshol=7 

7.6、調整大物件的標準

現象:老年代頻繁GC,每次回收的物件很多,而且單個物件的體積都比較大。

原因:如果大量的大物件直接分配到老年代,導致老年代容易被填滿而造成頻繁GC,可設定物件直接進入老年代的標準。

注意:這些大物件進入新生代後可能會使新生代的GC頻率和時間增加。

配置引數:

 //新生代可容納的最大物件,大於則直接會分配到老年代,0代表沒有限制。
  -XX:PretenureSizeThreshold=1000000 

7.7、調整GC的觸發時機

現象:CMS,G1 經常 Full GC,程式卡頓嚴重。

原因:G1和CMS 部分GC階段是併發進行的,業務執行緒和垃圾收集執行緒一起工作,也就說明垃圾收集的過程中業務執行緒會生成新的物件,所以在GC的時候需要預留一部分記憶體空間來容納新產生的物件,如果這個時候記憶體空間不足以容納新產生的物件,那麼JVM就會停止併發收集暫停所有業務執行緒(STW)來保證垃圾收集的正常執行。這個時候可以調整GC觸發的時機(比如在老年代佔用60%就觸發GC),這樣就可以預留足夠的空間來讓業務執行緒建立的物件有足夠的空間分配。

注意:提早觸發GC會增加老年代GC的頻率。

配置引數:

 //使用多少比例的老年代後開始CMS收集,預設是68%,如果頻繁發生SerialOld卡頓,應該調小
 -XX:CMSInitiatingOccupancyFraction
 ​
 //G1混合垃圾回收週期中要包括的舊區域設定佔用率閾值。預設佔用率為 65%
 -XX:G1MixedGCLiveThresholdPercent=65 

7.8、調整 JVM本地記憶體大小

現象:GC的次數、時間和回收的物件都正常,堆記憶體空間充足,但是報OOM

原因: JVM除了堆記憶體之外還有一塊堆外記憶體,這片記憶體也叫本地記憶體,可是這塊記憶體區域不足了並不會主動觸發GC,只有在堆記憶體區域觸發的時候順帶會把本地記憶體回收了,而一旦本地記憶體分配不足就會直接報OOM異常。

注意: 本地記憶體異常的時候除了上面的現象之外,異常資訊可能是OutOfMemoryError:Direct buffer memory。 解決方式除了調整本地記憶體大小之外,也可以在出現此異常時進行捕獲,手動觸發GC(System.gc())。

配置引數:

 XX:MaxDirectMemorySize

8、JVM調優例項

以下是整理自網路的一些JVM調優例項:

8.1、網站流量瀏覽量暴增後,網站反應頁面響很慢

1、問題推測:在測試環境測速度比較快,但是一到生產就變慢,所以推測可能是因為垃圾收集導致的業務執行緒停頓。

2、定位:為了確認推測的正確性,線上上通過jstat -gc 指令 看到JVM進行GC 次數頻率非常高,GC所佔用的時間非常長,所以基本推斷就是因為GC頻率非常高,所以導致業務執行緒經常停頓,從而造成網頁反應很慢。

3、解決方案:因為網頁訪問量很高,所以物件建立速度非常快,導致堆記憶體容易填滿從而頻繁GC,所以這裡問題在於新生代記憶體太小,所以這裡可以增加JVM記憶體就行了,所以初步從原來的2G記憶體增加到16G記憶體。

4、第二個問題:增加記憶體後的確平常的請求比較快了,但是又出現了另外一個問題,就是不定期的會間斷性的卡頓,而且單次卡頓的時間要比之前要長很多。

5、問題推測:練習到是之前的優化加大了記憶體,所以推測可能是因為記憶體加大了,從而導致單次GC的時間變長從而導致間接性的卡頓。

6、定位:還是通過jstat -gc 指令 檢視到 的確FGC次數並不是很高,但是花費在FGC上的時間是非常高的,根據GC日誌 檢視到單次FGC的時間有達到幾十秒的。

7、解決方案: 因為JVM預設使用的是PS+PO的組合,PS+PO垃圾標記和收集階段都是STW,所以記憶體加大了之後,需要進行垃圾回收的時間就變長了,所以這裡要想避免單次GC時間過長,所以需要更換併發類的收集器,因為當前的JDK版本為1.7,所以最後選擇CMS垃圾收集器,根據之前垃圾收集情況設定了一個預期的停頓的時間,上線後網站再也沒有了卡頓問題。

8.2、後臺匯出資料引發的OOM

問題描述:公司的後臺系統,偶發性的引發OOM異常,堆記憶體溢位。

1、因為是偶發性的,所以第一次簡單的認為就是堆記憶體不足導致,所以單方面的加大了堆記憶體從4G調整到8G。

2、但是問題依然沒有解決,只能從堆記憶體資訊下手,通過開啟了-XX:+HeapDumpOnOutOfMemoryError引數 獲得堆記憶體的dump檔案。

3、VisualVM 對 堆dump檔案進行分析,通過VisualVM檢視到佔用記憶體最大的物件是String物件,本來想跟蹤著String物件找到其引用的地方,但dump檔案太大,跟蹤進去的時候總是卡死,而String物件佔用比較多也比較正常,最開始也沒有認定就是這裡的問題,於是就從執行緒資訊裡面找突破點。

4、通過執行緒進行分析,先找到了幾個正在執行的業務執行緒,然後逐一跟進業務執行緒看了下程式碼,發現有個引起我注意的方法,匯出訂單資訊。

5、因為訂單資訊匯出這個方法可能會有幾萬的資料量,首先要從資料庫裡面查詢出來訂單資訊,然後把訂單資訊生成excel,這個過程會產生大量的String物件。

6、為了驗證自己的猜想,於是準備登入後臺去測試下,結果在測試的過程中發現到處訂單的按鈕前端居然沒有做點選後按鈕置灰互動事件,結果按鈕可以一直點,因為匯出訂單資料本來就非常慢,使用的人員可能發現點選後很久後頁面都沒反應,結果就一直點,結果就大量的請求進入到後臺,堆記憶體產生了大量的訂單物件和EXCEL物件,而且方法執行非常慢,導致這一段時間內這些物件都無法被回收,所以最終導致記憶體溢位。

7、知道了問題就容易解決了,最終沒有調整任何JVM引數,只是在前端的匯出訂單按鈕上加上了置灰狀態,等後端響應之後按鈕才可以進行點選,然後減少了查詢訂單資訊的非必要欄位來減少生成物件的體積,然後問題就解決了。

8.3、單個快取資料過大導致的系統CPU飈高

1、系統釋出後發現CPU一直飈高到600%,發現這個問題後首先要做的是定位到是哪個應用佔用CPU高,通過top 找到了對應的一個java應用佔用CPU資源600%。

2、如果是應用的CPU飈高,那麼基本上可以定位可能是鎖資源競爭,或者是頻繁GC造成的。

3、所以準備首先從GC的情況排查,如果GC正常的話再從執行緒的角度排查,首先使用jstat -gc PID 指令列印出GC的資訊,結果得到得到的GC 統計資訊有明顯的異常,應用在執行了才幾分鐘的情況下GC的時間就佔用了482秒,那麼問這很明顯就是頻繁GC導致的CPU飈高。

4、定位到了是GC的問題,那麼下一步就是找到頻繁GC的原因了,所以可以從兩方面定位了,可能是哪個地方頻繁建立物件,或者就是有記憶體洩露導致記憶體回收不掉。

5、根據這個思路決定把堆記憶體資訊dump下來看一下,使用jmap -dump 指令把堆記憶體資訊dump下來(堆記憶體空間大的慎用這個指令否則容易導致會影響應用,因為我們的堆記憶體空間才2G所以也就沒考慮這個問題了)。

6、把堆記憶體資訊dump下來後,就使用visualVM進行離線分析了,首先從佔用記憶體最多的物件中查詢,結果排名第三看到一個業務VO佔用堆記憶體約10%的空間,很明顯這個物件是有問題的。

7、通過業務物件找到了對應的業務程式碼,通過程式碼的分析找到了一個可疑之處,這個業務物件是檢視新聞資訊資訊生成的物件,由於想提升查詢的效率,所以把新聞資訊儲存到了redis快取裡面,每次呼叫資訊介面都是從快取裡面獲取。

8、把新聞儲存到redis快取裡面這個方式是沒有問題的,有問題的是新聞的50000多條資料都是儲存在一個key裡面,這樣就導致每次呼叫查詢新聞介面都會從redis裡面把50000多條資料都拿出來,再做篩選分頁拿出10條返回給前端。50000多條資料也就意味著會產生50000多個物件,每個物件280個位元組左右,50000個物件就有13.3M,這就意味著只要檢視一次新聞資訊就會產生至少13.3M的物件,那麼併發請求量只要到10,那麼每秒鐘都會產生133M的物件,而這種大物件會被直接分配到老年代,這樣的話一個2G大小的老年代記憶體,只需要幾秒就會塞滿,從而觸發GC。

9、知道了問題所在後那麼就容易解決了,問題是因為單個快取過大造成的,那麼只需要把快取減小就行了,這裡只需要把快取以頁的粒度進行快取就行了,每個key快取10條作為返回給前端1頁的資料,這樣的話每次查詢新聞資訊只會從快取拿出10條資料,就避免了此問題的 產生。

8.4、CPU經常100% 問題定位

問題分析:CPU高一定是某個程式長期佔用了CPU資源。

1、所以先需要找出那個進行佔用CPU高。

 top  列出系統各個程式的資源佔用情況。

2、然後根據找到對應進行裡哪個執行緒佔用CPU高。

 top -Hp 程式ID   列出對應程式裡面的執行緒佔用資源情況

3、找到對應執行緒ID後,再列印出對應執行緒的堆疊資訊

printf "%x\n"  PID    把執行緒ID轉換為16進位制。
 jstack PID 列印出程式的所有執行緒資訊,從列印出來的執行緒資訊中找到上一步轉換為16進位制的執行緒ID對應的執行緒資訊。

4、最後根據執行緒的堆疊資訊定位到具體業務方法,從程式碼邏輯中找到問題所在。

檢視是否有執行緒長時間的watting 或blocked
 如果執行緒長期處於watting狀態下, 關注watting on xxxxxx,說明執行緒在等待這把鎖,然後根據鎖的地址找到持有鎖的執行緒。

8.5、記憶體飈高問題定位

分析: 記憶體飈高如果是發生在java程式上,一般是因為建立了大量物件所導致,持續飈高說明垃圾回收跟不上物件建立的速度,或者記憶體洩露導致物件無法回收。

1、先觀察垃圾回收的情況

jstat -gc PID 1000 檢視GC次數,時間等資訊,每隔一秒列印一次。
  
 jmap -histo PID | head -20   檢視堆記憶體佔用空間最大的前20個物件型別,可初步檢視是哪個物件佔用了記憶體。

如果每次GC次數頻繁,而且每次回收的記憶體空間也正常,那說明是因為物件建立速度快導致記憶體一直佔用很高;如果每次回收的記憶體非常少,那麼很可能是因為記憶體洩露導致記憶體一直無法被回收。

2、匯出堆記憶體檔案快照

jmap -dump:live,format=b,file=/home/myheapdump.hprof PID  dump堆記憶體資訊到檔案。

3、使用visualVM對dump檔案進行離線分析,找到佔用記憶體高的物件,再找到建立該物件的業務程式碼位置,從程式碼和業務場景中定位具體問題。

8.6、資料分析平臺系統頻繁 Full GC

平臺主要對使用者在 App 中行為進行定時分析統計,並支援報表匯出,使用 CMS GC 演算法。

資料分析師在使用中發現系統頁面開啟經常卡頓,通過 jstat 命令發現系統每次 Young GC 後大約有 10% 的存活物件進入老年代。

原來是因為 Survivor 區空間設定過小,每次 Young GC 後存活物件在 Survivor 區域放不下,提前進入老年代。

通過調大 Survivor 區,使得 Survivor 區可以容納 Young GC 後存活物件,物件在 Survivor 區經歷多次 Young GC 達到年齡閾值才進入老年代。

調整之後每次 Young GC 後進入老年代的存活物件穩定執行時僅幾百 Kb,Full GC 頻率大大降低。

8.7、業務對接閘道器 OOM

閘道器主要消費 Kafka 資料,進行資料處理計算然後轉發到另外的 Kafka 佇列,系統執行幾個小時候出現 OOM,重啟系統幾個小時之後又 OOM。

通過 jmap 匯出堆記憶體,在 eclipse MAT 工具分析才找出原因:程式碼中將某個業務 Kafka 的 topic 資料進行日誌非同步列印,該業務資料量較大,大量物件堆積在記憶體中等待被列印,導致 OOM。

8.8、鑑權系統頻繁長時間 Full GC

系統對外提供各種賬號鑑權服務,使用時發現系統經常服務不可用,通過 Zabbix 的監控平臺監控發現系統頻繁發生長時間 Full GC,且觸發時老年代的堆記憶體通常並沒有佔滿,發現原來是業務程式碼中呼叫了 System.gc()。




參考:

【1】:周志明編著《深入理解Java虛擬機器:JVM高階特性與最佳實踐》

【2】:《實戰JAVA虛擬機器 JVM故障診斷與效能優化》

【3】:JVM效能調優詳解

【4】:如何合理的規劃一次jvm效能調優

【5】:關於GC原理和效能調優實踐,看這一篇就夠了

【6】:Java 應用效能調優實踐

【7】:JVM實戰:JVM調優策略

【8】:一般的Java專案需要JVM調優嗎?

【9】:Java8 JVM引數解讀

【10】:JVM引數設定-jdk8引數設定

【11】:JVM面試問題系列:JVM 配置常用引數和常用 GC 調優策略

【12】:Java1.8的jvm引數官方網站地址

相關文章