天天看點

深入了解JVM虛拟機——JVM參數以及調優實戰

作者:一個即将被退役的碼農
深入了解JVM虛拟機——JVM參數以及調優實戰

目錄

一. JVM優化的必要性

二. JVM調優原則

三. JVM運作參數設定

四. JVM性能調優工具

五. 常用調優政策

六. JVM調優執行個體

七. 一個具體的實戰案例分析

JVM優化的必要性

1.1: 項目上線後,什麼原因使得我們需要進行jvm調優

1)、垃圾太多(java線程,對象占滿記憶體),記憶體占滿了,程式跑不動了!!

2)、垃圾回收線程太多,頻繁地回收垃圾(垃圾回收線程本身也會占用資源: 占用記憶體,cpu資源),導緻程式性能下降

3)、回收垃圾頻繁導緻STW

是以基于以上的原因,程式上線後,必須進行調優,否則程式性能就無法提升;也就是程式上線後,必須設定合理的垃圾回收政策;

1.2: jvm調優的本質是什麼??

答案: 回收垃圾,及時回收沒有用垃圾對象,及時釋放掉記憶體空間

1.3: 基于伺服器環境,jvm堆記憶體到底應用設定多少記憶體?

1、32位的作業系統 --- 尋址能力 2^32 = 4GB ,最大的能支援4gb; jvm可以配置設定 2g+

2、64位的作業系統 --- 尋址能力 2^64 = 16384PB , 高性能計算機(IBM Z unix 128G 200+)

深入了解JVM虛拟機——JVM參數以及調優實戰

Jvm堆記憶體不能設定太大,否則會導緻尋址垃圾的時間過長,也就是導緻整個程式STW, 也不能設定太小,否則會導緻回收垃圾過于頻繁;

1.4:總結

如果你遇到以下情況,就需要考慮進行JVM調優了:

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

JVM調優原則

深入了解JVM虛拟機——JVM參數以及調優實戰

JVM調優是一個手段,但并不一定所有問題都可以通過JVM進行調優解決,是以,在進行JVM調優時,我們要遵循一些原則:

  • 大多數的Java應用不需要進行JVM優化;
  • 大多數導緻GC問題的原因是代碼層面的問題導緻的(代碼層面);
  • 上線之前,應先考慮将機器的JVM參數設定到最優;
  • 減少建立對象的數量(代碼層面);
  • 減少使用全局變量和大對象(代碼層面);
  • 優先架構調優和代碼調優,JVM優化是不得已的手段(代碼、架構層面);
  • 分析GC情況優化代碼比優化JVM參數更好(代碼層面);

通過以上原則,我們發現,其實最有效的優化手段是架構和代碼層面的優化,而JVM優化則是最後不得已的手段,也可以說是對伺服器配置的最後一次“壓榨”。

2.1 JVM調優目标

調優的最終目的都是為了令應用程式使用最小的硬體消耗來承載更大的吞吐。jvm調優主要是針對垃圾收集器的收集性能優化,令運作在虛拟機上的應用能夠使用更少的記憶體以及延遲擷取更大的吞吐量。

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

其中,任何一個屬性性能的提高,幾乎都是以犧牲其他屬性性能的損為代價的,不可兼得。具體根據在業務中的重要性确定。

2.2 JVM調優量化目标

下面展示了一些JVM調優的量化目标參考執行個體:

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

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

2.3 JVM調優的步驟

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

  • 分析GC日志及dump檔案,判斷是否需要優化,确定瓶頸問題點;
  • 确定JVM調優量化目标;
  • 确定JVM調優參數(根據曆史JVM參數來調整);
  • 依次調優記憶體、延遲、吞吐量等名額;
  • 對比觀察調優前後的差異;
  • 不斷的分析和調整,直到找到合适的JVM參數配置;
  • 找到最合适的參數,将這些參數應用到所有伺服器,并進行後續跟蹤。

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

深入了解JVM虛拟機——JVM參數以及調優實戰

調優原則總結

JVM的自動記憶體管理本來就是為了将開發人員從記憶體管理的泥潭裡拉出來。JVM調優不是正常手段,性能問題一般第一選擇是優化程式,最後的選擇才是進行JVM調優。

即使不得不進行JVM調優,也絕對不能拍腦門就去調整參數,一定要全面監控,詳細分析性能資料。

附錄:系統性能優化指導

深入了解JVM虛拟機——JVM參數以及調優實戰

JVM運作參數設定

3.1、堆參數設定

-XX:+PrintGC 使用這個參數,虛拟機啟動後,隻要遇到GC就會列印日志

-XX:+UseSerialGC 配置串行回收器

-XX:+PrintGCDetails 可以檢視詳細資訊,包括各個區的情況

-Xms 設定Java程式啟動時初始化堆大小

-Xmx 設定Java程式能獲得最大的堆大小

-Xmx20m -Xms5m -XX:+PrintCommandLineFlags 可以将隐式或者顯示傳給虛拟機的參數輸出

3.2、新生代參數配置

-Xmn 可以設定新生代的大小,設定一個比較大的新生代會減少老年代的大小,這個參數對系統性能以及GC行為有很大的影響,新生代大小一般會設定整個堆空間的1/3到1/4左右

-XX:SurvivorRatio 用來設定新生代中eden空間和from/to空間的比例。含義:-XX:SurvivorRatio=eden/from**/**eden/to

不同的堆分布情況,對系統執行會産生一定的影響,在實際工作中,應該根據系統的特點做出合理的配置,基本政策:盡可能将對象預留在新生代,減少老年代的GC次數

除了可以設定新生代的絕對大小(-Xmn),還可以使用(-XX:NewRatio)設定新生代和老年代的比例:-XX:NewRatio=老年代/新生代

配置運作時參數:

-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC

3.3、堆溢出參數配置

在Java程式在運作過程中,如果對空間不足,則會抛出記憶體溢出的錯誤(Out Of Memory)OOM,一旦這類問題發生在生産環境,則可能引起嚴重的業務中斷,Java虛拟機提供了-XX:+HeapDumpOnOutOfMemoryError,使用該參數可以在記憶體溢出時導出整個堆資訊,與之配合使用的還有參數-XX:HeapDumpPath,可以設定導出堆的存放路徑

記憶體分析工具:Memory Analyzer

配置運作時參數-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/Demo3.dump

3.4、棧參數配置

Java虛拟機提供了參數-Xss來指定線程的最大棧空間,整個參數也直接決定了函數可調用的最大深度。

配置運作時參數:-Xss1m

3.5、方法區參數配置

和Java堆一樣,方法區是一塊所有線程共享的記憶體區域,它用于儲存系統的類資訊,方法區(永久區)可以儲存多少資訊可以對其進行配置,在預設情況下,-XX:MaxPermSize為64M,如果系統運作時生産大量的類,就需要設定一個相對合适的方法區,以免出現永久區記憶體溢出的問題

-XX:PermSize=64M -XX:MaxPermSize=64M

3.6、直接記憶體參數配置

直接記憶體也是Java程式中非常重要的組成部分,特别是廣泛用在NIO中,直接記憶體跳過了Java堆,使用Java程式可以直接通路原生堆空間,是以在一定程度上加快了記憶體空間的通路速度

但是說直接記憶體一定就可以提高記憶體通路速度也不見得,具體情況具體分析

相關配置參數:-XX:MaxDirectMemorySize,如果不設定,預設值為最大堆空間,即-Xmx。直接記憶體使用達到上限時,就會觸發垃圾回收,如果不能有效的釋放空間,就會引起系統的OOM

3.7、對象進入老年代的參數配置

一般而言,對象首次建立會被放置在新生代的eden區,如果沒有GC介入,則對象不會離開eden區,那麼eden區的對象如何進入老年代呢?

通常情況下,隻要對象的年齡達到一定的大小,就會自動離開年輕代進入老年代,對象年齡是由對象經曆數次GC決定的,在新生代每次GC之後如果對象沒有被回收,則年齡加1

虛拟機提供了一個參數來控制新生代對象的最大年齡,當超過這個年齡範圍就會晉升老年代

-XX:MaxTenuringThreshold,預設情況下為15

配置運作時參數:-Xmx64M -Xms64M -XX:+PrintGCDetails

結論:對象首次建立會被放置在新生代的eden區,是以輸出結果中from和to區都為0%

根據設定MaxTenuringThreshold參數,可以指定新生代對象經過多少次回收後進入老年代。另外,大對象新生代eden區無法裝入時,也會直接進入老年代。

JVM裡有個參數可以設定對象的大小超過在指定的大小之後,直接晉升老年代-XX:PretenureSizeThreshold=15

參數:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails

使用PretenureSizeThreshold可以進行指定進入老年代的對象大小,但是要注意TLAB區域優先配置設定空間。虛拟機對于體積不大的對象 會優先把資料配置設定到TLAB區域中,是以就失去了在老年代配置設定的機會

參數:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB

3.8、TLAB參數配置

TLAB全稱是Thread Local Allocation Buffer,即線程本地配置設定緩存,從名字上看是一個線程專用的記憶體配置設定區域,是為了加速對象配置設定對象而生的。每一個線程都會産生一個TLAB,該線程獨享的工作區域,Java虛拟機使用這種TLAB區來避免多線程沖突問題,提高了對象配置設定的效率

TLAB空間一般不會太大,當大對象無法在TLAB配置設定時,則會直接配置設定到堆上

-XX:+UseTLAB 使用TLAB

-XX:+TLABSize 設定TLAB大小

-XX:TLABRefillWasteFraction 設定維護進入TLAB空間的單個對象大小,它是一個比例值,預設為64,即如果對象大于整個空間的1/64,則在堆建立對象

-XX:+PrintTLAB 檢視TLAB資訊

-XX:ResizeTLAB 自調整TLABRefillWasteFraction門檻值

參數:-XX:+UseTLAB -XX:+PrintTLAB -XX:+PrintGC -XX:TLABSize=102400 -XX:-ResizeTLAB -XX:TLABRefillWasteFraction=100 -XX:-DoEscapeAnalysis -server

記憶體參數

參數 含義 預設值 示例 說明
-Xms 初始堆大小 實體記憶體的1/64(<1GB) -Xms1g 預設(MinHeapFreeRatio參數可以調整)空餘堆記憶體小于40%時,JVM就會增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 實體記憶體的1/4(<1GB) -Xmx1g 預設(MaxHeapFreeRatio參數可以調整)空餘堆記憶體大于70%時,JVM會減少堆直到 -Xms的最小限制
-Xmn 年輕代大小 -Xmn512m

注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。

整個堆大小=年輕代大小 + 年老代大小 + 持久代大小.

增大年輕代後,将會減小年老代大小.此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8

-XX:NewRatio 年輕代與年老代的比值 -XX:NewRatio=1

-XX:NewRatio=4表示年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5

Xms=Xmx并且設定了Xmn的情況下,該參數不需要進行設定。

-XX:SurvivorRatio Eden區與Survivor區的大小比值 預設8:1:1 設定為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區占整個年輕代的1/10
-Xss 每個線程的堆棧大小

JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K.更具應用的線程所需記憶體大小進行 調整.在相同實體記憶體下,減小這個值能生成更多的線程.但是作業系統對一個程序内的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右

一般小的應用, 如果棧不是很深, 應該是128k夠用的 大的應用建議使用256k。這個選項對性能影響比較大,需要嚴格的測試。

-XX:MetaspaceSize 初始中繼資料空間大小
-XX:MaxMetaspaceSize=128m 最大中繼資料空間大小

四.JVM性能調優工具

這個篇幅在這裡就不過多介紹了,可以參照:

深入了解JVM虛拟機——Java虛拟機的監控及診斷工具大全

五.常用調優政策

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

5.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

5.2、調整記憶體大小

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

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

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

參數配置:

//設定堆初始值

指令1:-Xms2g

指令2:-XX:InitialHeapSize=2048m

//設定堆區最大值

指令1:`-Xmx2g`

指令2: -XX:MaxHeapSize=2048m

//新生代記憶體配置

指令1:-Xmn512m

指令2:-XX:MaxNewSize=512m

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

現象:程式間接性的卡頓

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

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

參數配置:

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

-XX:MaxGCPauseMillis

5.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

5.5、調整對象升老年代的年齡

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

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

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

配置參數:

//進入老年代最小的GC年齡,年輕代對象轉換為老年代對象最小年齡值,預設值7

-XX:InitialTenuringThreshol=7

5.6、調整大對象的标準

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

原因:如果大量的大對象直接配置設定到老年代,導緻老年代容易被填滿而造成頻繁GC,可設定對象直接進入老年代的标準。

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

配置參數:

//新生代可容納的最大對象,大于則直接會配置設定到老年代,0代表沒有限制。

-XX:PretenureSizeThreshold=1000000

5.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

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

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

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

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

配置參數:

XX:MaxDirectMemorySize

六、JVM調優執行個體

整理的一些JVM調優執行個體:

6.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垃圾收集器,根據之前垃圾收集情況設定了一個預期的停頓的時間,上線後網站再也沒有了卡頓問題。

6.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參數,隻是在前端的導出訂單按鈕上加上了置灰狀态,等後端響應之後按鈕才可以進行點選,然後減少了查詢訂單資訊的非必要字段來減少生成對象的體積,然後問題就解決了。

6.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條資料,就避免了此問題的 産生。

6.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,說明線程在等待這把鎖,然後根據鎖的位址找到持有鎖的線程。

6.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檔案進行離線分析,找到占用記憶體高的對象,再找到建立該對象的業務代碼位置,從代碼和業務場景中定位具體問題。

6.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 頻率大大降低。

6.7、業務對接網關 OOM

網關主要消費 Kafka 資料,進行資料處理計算然後轉發到另外的 Kafka 隊列,系統運作幾個小時候出現 OOM,重新開機系統幾個小時之後又 OOM。

通過 jmap 導出堆記憶體,在 eclipse MAT 工具分析才找出原因:代碼中将某個業務 Kafka 的 topic 資料進行日志異步列印,該業務資料量較大,大量對象堆積在記憶體中等待被列印,導緻 OOM。

6.8、鑒權系統頻繁長時間 Full GC

系統對外提供各種賬号鑒權服務,使用時發現系統經常服務不可用,通過 Zabbix 的監控平台監控發現系統頻繁發生長時間 Full GC,且觸發時老年代的堆記憶體通常并沒有占滿,發現原來是業務代碼中調用了

七、一個具體的實戰案例分析

7.1 典型調優參數設定

伺服器配置: 4cpu,8GB記憶體 ---- jvm調優實際上是設定一個合理大小的jvm堆記憶體(既不能太大,也不能太小)

-Xmx3550m 設定jvm堆記憶體最大值 (經驗值設定: 根據壓力測試,根據線上程式運作效果情況)

-Xms3550m 設定jvm堆記憶體初始化大小,一般情況下必須設定此值和最大的最大的堆記憶體空間保持一緻,防止記憶體抖動,消耗性能

-Xmn2g 設定年輕代占用的空間大小

-Xss256k 設定線程堆棧的大小;jdk5.0以後預設線程堆棧大小為1MB; 在相同的記憶體情況下,減小堆棧大小,可以使得作業系統建立更多的業務線程;

jvm堆記憶體設定:

nohup java -Xmx3550m -Xms3550m -Xmn2g -Xss256k -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1

TPS性能曲線:

深入了解JVM虛拟機——JVM參數以及調優實戰

7.2 分析gc日志

如果需要分析gc日志,就必須使得服務gc輸入gc詳情到log日志檔案中,然後使用相應gc日志分析工具來對日志進行分析即可;

把gc詳情輸出到一個gc.log日志檔案中,便于gc分析

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log

Throughput: 業務線程執行時間 / (gc時間+業務線程時間)

深入了解JVM虛拟機——JVM參數以及調優實戰

分析gc日志,發現,一開始就發生了3次fullgc,很明顯jvm優化參數的設定是有問題的;

深入了解JVM虛拟機——JVM參數以及調優實戰

檢視fullgc發生問題原因: jstat -gcutil pid

深入了解JVM虛拟機——JVM參數以及調優實戰

Metaspace持久代: 初始化配置設定大小20m , 當metaspace被占滿後,必須對持久代進行擴容,如果metaspace每進行一次擴容,fullgc就需要執行一次;(fullgc回收整個堆空間,非常占用時間)

調整gc配置: 修改永久代空間初始化大小:

nohup java -Xmx3550m -Xms3550m -Xmn2g -Xss256k -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1

經過調優後,fullgc現象已經消失了:

深入了解JVM虛拟機——JVM參數以及調優實戰

7.3 Young&Old比例

年輕代和老年代比例:1:2 參數:-XX:NewRetio = 4 , 表示年輕代(eden,s0,s1)和老年代所占比值為1:4

1) -XX:NewRetio = 4

深入了解JVM虛拟機——JVM參數以及調優實戰

年輕代配置設定的記憶體大小變小了,這樣YGC次數變多了,雖然fullgc不發生了,但是YGC花費的時間更多了!

2) -XX:NewRetio = 2 YGC發生的次數必然會減少;因為eden區域的大小變大了,是以YGC就會變少;

深入了解JVM虛拟機——JVM參數以及調優實戰

7.4 Eden&S0S1

為了進一步減少YGC, 可以設定 enden ,s 區域的比值大小; 設定方式: -XX:SurvivorRatio=8

1) 設定比值:8:1:1

深入了解JVM虛拟機——JVM參數以及調優實戰

2) Xmn2g 8:1:1

nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1

根據gc調優,垃圾回收次數,時間,吞吐量都是一個比較優的一個配置;

深入了解JVM虛拟機——JVM參數以及調優實戰

7.5 吞吐量優先

使用并行的垃圾回收器,可以充分利用多核心cpu來幫助進行垃圾回收;這樣的gc方式,就叫做吞吐量優先的調優方式

垃圾回收器組合: ps(parallel scavenge) + po (parallel old) 此垃圾回收器是Jdk1.8 預設的垃圾回收器組合;

nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseParallelGC -XX:UseParallelOldGC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1

7.6 響應時間優先

使用cms垃圾回收器,就是一個響應時間優先的組合; cms垃圾回收器(垃圾回收和業務線程交叉執行,不會讓業務線程進行停頓stw)盡可能的減少stw的時間,是以使用cms垃圾回收器組合,是響應時間優先組合

nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseParNewGC -XX:UseConcMarkSweepGC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1

可以發現,cms垃圾回收器時間變長;

7.7 g1

配置方式如下所示:

nohup java -Xmx3550m -Xms3550m -Xmn2g -XX:SurvivorRatio=8 -Xss256k -XX:+UseG1GC -XX:MetaspaceSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar jshop-web-1.0-SNAPSHOT.jar --spring.addition-location=application.yaml > jshop.log 2>&1
深入了解JVM虛拟機——JVM參數以及調優實戰

繼續閱讀