1、調優原則
JVM調優聽起來很高大上,但是要認識到,JVM調優應該是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參數配置;
- 找到最合適的參數,將這些參數應用到所有服務器,並進行后續跟蹤。
以上操作步驟中,某些步驟是需要多次不斷迭代完成的。一般是從滿足程序的內存使用需求開始的,之后是時間延遲的要求,最后才是吞吐量的要求,要基於這個步驟來不斷優化,每一個步驟都是進行下一步的基礎,不可逆行。
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自帶了很多性能監控工具,我們可以用這些工具來監測系統和排查內存性能問題。
6.2、Linux 命令行工具
進行性能監控和問題排查的時候,常常是結合操作系統本身的命令行工具來進行。
命令 | 說明 |
---|---|
top | 實時顯示正在執行進程的 CPU 使用率、內存使用率以及系統負載等信息 |
vmstat | 對操作系統的虛擬內存、進程、CPU活動進行監控 |
pidstat | 監控指定進程的上下文切換 |
iostat | 監控磁盤IO |
其它還有一些第三方的監控工具,同樣是性能分析和故障排查的利器,如MAT、GChisto、JProfiler、arthas。
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性能調優
【6】:Java 應用性能調優實踐
【7】:JVM實戰:JVM調優策略
【9】:Java8 JVM參數解讀
【10】:JVM參數設置-jdk8參數設置
【11】:JVM面試問題系列:JVM 配置常用參數和常用 GC 調優策略
【12】:Java1.8的jvm參數官方網站地址