JVM性能調優實踐

點擊上方藍色字體,選擇「標星公衆號」
php

優質文章,第一時間送達html

  做者 |  滴哩哩哩滴哩哩哩噠噠java

來源 |  urlify.cn/mqMVvuweb

76套java從入門到精通實戰課程分享數據庫

一、JVM調優目標:windows

使用較小的內存佔用來得到較高的吞吐量或者較低的延遲。數組

程序在上線前的測試或運行中有時會出現一些大大小小的JVM問題,好比cpu load太高、請求延遲、tps下降等,甚至出現內存泄漏(每次垃圾收集使用的時間愈來愈長,垃圾收集頻率愈來愈高,每次垃圾收集清理掉的垃圾數據愈來愈少)、內存溢出致使系統崩潰,所以須要對JVM進行調優,使得程序在正常運行的前提下,得到更高的用戶體驗和運行效率。瀏覽器

這裏有幾個比較重要的指標:緩存

  • 內存佔用:程序正常運行須要的內存大小。服務器

  • 延遲:因爲垃圾收集而引發的程序停頓時間。

  • 吞吐量:用戶程序運行時間佔用戶程序和垃圾收集佔用總時間的比值。

固然,和CAP原則同樣,同時知足一個程序內存佔用小、延遲低、高吞吐量是不可能的,程序的目標不一樣,調優時所考慮的方向也不一樣,在調優以前,必需要結合實際場景,有明確的的優化目標,找到性能瓶頸,對瓶頸有針對性的優化,最後進行測試,經過各類監控工具確認調優後的結果是否符合目標。

二、JVM調優工具

(1)調優能夠依賴、參考的數據有系統運行日誌、堆棧錯誤信息、gc日誌、線程快照、堆轉儲快照等。

①系統運行日誌:系統運行日誌就是在程序代碼中打印出的日誌,描述了代碼級別的系統運行軌跡(執行的方法、入參、返回值等),通常系統出現問題,系統運行日誌是首先要查看的日誌。

②堆棧錯誤信息:當系統出現異常後,能夠根據堆棧信息初步定位問題所在,好比根據「java.lang.OutOfMemoryError: Java heap space」能夠判斷是堆內存溢出;根據「java.lang.StackOverflowError」能夠判斷是棧溢出;根據「java.lang.OutOfMemoryError: PermGen space」能夠判斷是方法區溢出等。

③GC日誌:程序啓動時用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 能夠在程序運行時把gc的詳細過程記錄下來,或者直接配置「-verbose:gc」參數把gc日誌打印到控制檯,經過記錄的gc日誌能夠分析每塊內存區域gc的頻率、時間等,從而發現問題,進行有針對性的優化。 
好比以下一段GC日誌:

  
  
  
   
   
            
   
   
2018-08-02T14:39:11.560-0800: 10.171: [GC [PSYoungGen: 30128K->4091K(30208K)] 51092K->50790K(98816K), 0.0140970 secs] [Times: user=0.02 sys=0.03, real=0.01 secs] 
2018-08-02T14:39:11.574-0800: 10.185: [Full GC [PSYoungGen: 4091K->0K(30208K)] [ParOldGen: 46698K->50669K(68608K)] 50790K->50669K(98816K) [PSPermGen: 2635K->2634K(21504K)], 0.0160030 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
2018-08-02T14:39:14.045-0800: 12.656: [GC [PSYoungGen: 14097K->4064K(30208K)] 64766K->64536K(98816K), 0.0117690 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2018-08-02T14:39:14.057-0800: 12.668: [Full GC [PSYoungGen: 4064K->0K(30208K)] [ParOldGen: 60471K->401K(68608K)] 64536K->401K(98816K) [PSPermGen: 2634K->2634K(21504K)], 0.0102020 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

上面一共是4條GC日誌,來看第一行日誌,「2018-08-02T14:39:11.560-0800」是精確到了毫秒級別的UTC 通用標準時間格式,配置了「-XX:+PrintGCDateStamps」這個參數能夠跟隨gc日誌打印出這種時間戳,「10.171」是從JVM啓動到發生gc通過的秒數。第一行日誌正文開頭的「[GC」說明此次GC沒有發生Stop-The-World(用戶線程停頓),第二行日誌正文開頭的「[Full GC」說明此次GC發生了Stop-The-World,因此說,[GC和[Full GC跟新生代和老年代不要緊,和垃圾收集器的類型有關係,若是直接調用System.gc(),將顯示[Full GC(System)。接下來的「[PSYoungGen」、「[ParOldGen」表示GC發生的區域,具體顯示什麼名字也跟垃圾收集器有關,好比這裏的「[PSYoungGen」表示Parallel Scavenge收集器,「[ParOldGen」表示Serial Old收集器,此外,Serial收集器顯示「[DefNew」,ParNew收集器顯示「[ParNew」等。再日後的「30128K->4091K(30208K)」表示進行了此次gc後,該區域的內存使用空間由30128K減少到4091K,總內存大小爲30208K。每一個區域gc描述後面的「51092K->50790K(98816K), 0.0140970 secs」進行了此次垃圾收集後,整個堆內存的內存使用空間由51092K減少到50790K,整個堆內存總空間爲98816K,gc耗時0.0140970秒。

④線程快照:顧名思義,根據線程快照能夠看到線程在某一時刻的狀態,當系統中可能存在請求超時、死循環、死鎖等狀況是,能夠根據線程快照來進一步肯定問題。經過執行虛擬機自帶的「jstack pid」命令,能夠dump出當前進程中線程的快照信息,更詳細的使用和分析網上有不少例,這篇文章寫到這裏已經很長了就不過多敘述了,貼一篇博客供參考:http://www.cnblogs.com/kongzhongqijing/articles/3630264.html

⑤堆轉儲快照:程序啓動時可使用 「-XX:+HeapDumpOnOutOfMemory」 和 「-XX:HeapDumpPath=/data/jvm/dumpfile.hprof」,當程序發生內存溢出時,把當時的內存快照以文件形式進行轉儲(也能夠直接用jmap命令轉儲程序運行時任意時刻的內存快照),過後對當時的內存使用狀況進行分析。

(2)JVM調優工具

①用 jps(JVM process Status)能夠查看虛擬機啓動的全部進程、執行主類的全名、JVM啓動參數,好比當執行了JPSTest類中的main方法後(main方法持續執行),執行 jps -l可看到下面的JPSTest類的pid爲31354,加上-v參數還能夠看到JVM啓動參數。

  
  
  
   
   
            
   
   

3265 
32914 sun.tools.jps.Jps
31353 org.jetbrains.jps.cmdline.Launcher
31354 com.danny.test.code.jvm.JPSTest
380

 ②用jstat(JVM Statistics Monitoring Tool)監視虛擬機信息 
jstat -gc pid 500 10 :每500毫秒打印一次Java堆情況(各個區的容量、使用容量、gc時間等信息),打印10次

  
  
  
   
   
            
   
   

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
11264.0 11264.0 11202.7  0.0   11776.0   1154.3   68608.0    36238.7     -      -      -      -        14    0.077   7      0.049    0.126
11264.0 11264.0 11202.7  0.0   11776.0   4037.0   68608.0    36238.7     -      -      -      -        14    0.077   7      0.049    0.126
11264.0 11264.0 11202.7  0.0   11776.0   6604.5   68608.0    36238.7     -      -      -      -        14    0.077   7      0.049    0.126
11264.0 11264.0 11202.7  0.0   11776.0   9487.2   68608.0    36238.7     -      -      -      -        14    0.077   7      0.049    0.126
11264.0 11264.0  0.0    0.0   11776.0   258.1    68608.0    58983.4     -      -      -      -        15    0.082   8      0.059    0.141
11264.0 11264.0  0.0    0.0   11776.0   3076.8   68608.0    58983.4     -      -      -      -        15    0.082   8      0.059    0.141
11264.0 11264.0  0.0    0.0   11776.0    0.0     68608.0     390.0      -      -      -      -        16    0.084   9      0.066    0.149
11264.0 11264.0  0.0    0.0   11776.0    0.0     68608.0     390.0      -      -      -      -        16    0.084   9      0.066    0.149
11264.0 11264.0  0.0    0.0   11776.0   258.1    68608.0     390.0      -      -      -      -        16    0.084   9      0.066    0.149
11264.0 11264.0  0.0    0.0   11776.0   3012.8   68608.0     390.0      -      -      -      -        16    0.084   9      0.066    0.149

jstat還能夠以其餘角度監視各區內存大小、監視類裝載信息等,具體能夠google jstat的詳細用法。

③用jmap(Memory Map for Java)查看堆內存信息 
執行jmap -histo pid能夠打印出當前堆中全部每一個類的實例數量和內存佔用,以下,class name是每一個類的類名([B是byte類型,[C是char類型,[I是int類型),bytes是這個類的全部示例佔用內存大小,instances是這個類的實例數量:

  
  
  
   
   
            
   
   

num     #instances         #bytes  class name
----------------------------------------------
  1:          2291       29274080  [B
  2:         15252        1961040  <methodKlass>
  3:         15252        1871400  <constMethodKlass>
  4:         18038         721520  java.util.TreeMap$Entry
  5:          6182         530088  [C
  6:         11391         273384  java.lang.Long
  7:          5576         267648  java.util.TreeMap
  8:            50         155872  [I
  9:          6124         146976  java.lang.String
 10:          3330         133200  java.util.LinkedHashMap$Entry
 11:          5544         133056  javax.management.openmbean.CompositeDataSupport

執行 jmap -dump 能夠轉儲堆內存快照到指定文件,好比執行 jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof 3361 能夠把當前堆內存的快照轉儲到dumpfile_jmap.hprof文件中,而後能夠對內存快照進行分析。

④利用jconsole、jvisualvm分析內存信息(各個區如Eden、Survivor、Old等內存變化狀況),若是查看的是遠程服務器的JVM,程序啓動須要加上以下參數:

  
  
  
   
   
            
   
   

"-Dcom.sun.management.jmxremote=true" 
"-Djava.rmi.server.hostname=12.34.56.78" 
"-Dcom.sun.management.jmxremote.port=18181" 
"-Dcom.sun.management.jmxremote.authenticate=false" 
"-Dcom.sun.management.jmxremote.ssl=false"

下圖是jconsole界面,概覽選項能夠觀測堆內存使用量、線程數、類加載數和CPU佔用率;內存選項能夠查看堆中各個區域的內存使用量和左下角的詳細描述(內存大小、GC狀況等);線程選項能夠查看當前JVM加載的線程,查看每一個線程的堆棧信息,還能夠檢測死鎖;VM概要描述了虛擬機的各類詳細參數。(jconsole功能演示) 

下圖是jvisualvm的界面,功能比jconsole略豐富一些,不過大部分功能都須要安裝插件。概述跟jconsole的VM概要差很少,描述的是jvm的詳細參數和程序啓動參數;監視展現的和jconsole的概覽界面差很少(CPU、堆/方法區、類加載、線程);線程和jconsole的線程界面差很少;抽樣器能夠展現當前佔用內存的類的排行榜及其實例的個數;Visual GC能夠更豐富地展現當前各個區域的內存佔用大小及歷史信息(下圖)。(jvisualvm功能演示) 

⑤分析堆轉儲快照

前面說到配置了 「-XX:+HeapDumpOnOutOfMemory」 參數能夠在程序發生內存溢出時dump出當前的內存快照,也能夠用jmap命令隨時dump出當時內存狀態的快照信息,dump的內存快照通常是以.hprof爲後綴的二進制格式文件。 
能夠直接用 jhat(JVM Heap Analysis Tool) 命令來分析內存快照,它的本質實際上內嵌了一個微型的服務器,能夠經過瀏覽器來分析對應的內存快照,好比執行 jhat -port 9810 -J-Xmx4G /data/jvm/dumpfile_jmap.hprof 表示以9810端口啓動 jhat 內嵌的服務器:

  
  
  
   
   
            
   
   

Reading from /Users/dannyhoo/data/jvm/dumpfile_jmap.hprof...
Dump file created Fri Aug 03 15:48:27 CST 2018
Snapshot read, resolving...
Resolving 276472 objects...
Chasing references, expect 55 dots.......................................................
Eliminating duplicate references.......................................................
Snapshot resolved.
Started HTTP server on port 9810
Server is ready.

在控制檯能夠看到服務器啓動了,訪問 http://127.0.0.1:9810/ 能夠看到對快照中的每一個類進行分析的結果(界面略low),下圖是我隨便選擇了一個類的信息,有這個類的父類,加載這個類的類加載器和佔用的空間大小,下面還有這個類的每一個實例(References)及其內存地址和大小,點進去會顯示這個實例的一些成員變量等信息: 

jvisualvm也能夠分析內存快照,在jvisualvm菜單的「文件」-「裝入」,選擇堆內存快照,快照中的信息就以圖形界面展現出來了,以下,主要能夠查看每一個類佔用的空間、實例的數量和實例的詳情等: 

還有不少分析內存快照的第三方工具,好比eclipse mat,它比jvisualvm功能更專業,出了查看每一個類及對應實例佔用的空間、數量,還能夠查詢對象之間的調用鏈,能夠查看某個實例到GC Root之間的鏈,等等。能夠在eclipse中安裝mat插件,也能夠下載獨立的版本(http://www.eclipse.org/mat/downloads.php ),我在mac上安裝後運行起來老卡死~下面是在windows上的截圖(MAT功能演示):

(3)JVM調優經驗

JVM配置方面,通常狀況能夠先用默認配置(基本的一些初始參數能夠保證通常的應用跑的比較穩定了),在測試中根據系統運行情況(會話併發狀況、會話時間等),結合gc日誌、內存監控、使用的垃圾收集器等進行合理的調整,當老年代內存太小時可能引發頻繁Full GC,當內存過大時Full GC時間會特別長。

那麼JVM的配置好比新生代、老年代應該配置多大最合適呢?答案是不必定,調優就是找答案的過程,物理內存必定的狀況下,新生代設置越大,老年代就越小,Full GC頻率就越高,但Full GC時間越短;相反新生代設置越小,老年代就越大,Full GC頻率就越低,但每次Full GC消耗的時間越大。建議以下:

  • -Xms和-Xmx的值設置成相等,堆大小默認爲-Xms指定的大小,默認空閒堆內存小於40%時,JVM會擴大堆到-Xmx指定的大小;空閒堆內存大於70%時,JVM會減少堆到-Xms指定的大小。若是在Full GC後知足不了內存需求會動態調整,這個階段比較耗費資源。

  • 新生代儘可能設置大一些,讓對象在新生代多存活一段時間,每次Minor GC 都要儘量多的收集垃圾對象,防止或延遲對象進入老年代的機會,以減小應用程序發生Full GC的頻率。

  • 老年代若是使用CMS收集器,新生代能夠不用太大,由於CMS的並行收集速度也很快,收集過程比較耗時的併發標記和併發清除階段均可以與用戶線程併發執行。

  • 方法區大小的設置,1.6以前的須要考慮系統運行時動態增長的常量、靜態變量等,1.7只要差很少能裝下啓動時和後期動態加載的類信息就行。

代碼實現方面,性能出現問題好比程序等待、內存泄漏除了JVM配置可能存在問題,代碼實現上也有很大關係:

  • 避免建立過大的對象及數組:過大的對象或數組在新生代沒有足夠空間容納時會直接進入老年代,若是是短命的大對象,會提早出發Full GC。

  • 避免同時加載大量數據,如一次從數據庫中取出大量數據,或者一次從Excel中讀取大量記錄,能夠分批讀取,用完儘快清空引用。

  • 當集合中有對象的引用,這些對象使用完以後要儘快把集合中的引用清空,這些無用對象儘快回收避免進入老年代。

  • 能夠在合適的場景(如實現緩存)採用軟引用、弱引用,好比用軟引用來爲ObjectA分配實例:SoftReference objectA=new SoftReference(); 在發生內存溢出前,會將objectA列入回收範圍進行二次回收,若是此次回收尚未足夠內存,纔會拋出內存溢出的異常。 
    避免產生死循環,產生死循環後,循環體內可能重複產生大量實例,致使內存空間被迅速佔滿。

  • 儘可能避免長時間等待外部資源(數據庫、網絡、設備資源等)的狀況,縮小對象的生命週期,避免進入老年代,若是不能及時返回結果能夠適當採用異步處理的方式等。

(4)JVM問題排查記錄案例

JVM服務問題排查 https://blog.csdn.net/jacin1/article/details/44837595

次讓人難以忘懷的排查頻繁Full GC過程 http://caogen81.iteye.com/blog/1513345

線上FullGC頻繁的排查 https://blog.csdn.net/wilsonpeng3/article/details/70064336/

【JVM】線上應用故障排查 https://www.cnblogs.com/Dhouse/p/7839810.html

一次JVM中FullGC問題排查過程 http://iamzhongyong.iteye.com/blog/1830265

JVM內存溢出致使的CPU太高問題排查案例 https://blog.csdn.net/nielinqi520/article/details/78455614

一個java內存泄漏的排查案例 https://blog.csdn.net/aasgis6u/article/details/54928744

(5)經常使用JVM參數參考:

參數 說明 實例
-Xms 初始堆大小,默認物理內存的1/64 -Xms512M
-Xmx 最大堆大小,默認物理內存的1/4 -Xms2G
-Xmn 新生代內存大小,官方推薦爲整個堆的3/8 -Xmn512M
-Xss 線程堆棧大小,jdk1.5及以後默認1M,以前默認256k -Xss512k
-XX:NewRatio=n 設置新生代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4 -XX:NewRatio=3
-XX:SurvivorRatio=n 年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:8,表示Eden:Survivor=8:1:1,一個Survivor區佔整個年輕代的1/8 -XX:SurvivorRatio=8
-XX:PermSize=n 永久代初始值,默認爲物理內存的1/64 -XX:PermSize=128M
-XX:MaxPermSize=n 永久代最大值,默認爲物理內存的1/4 -XX:MaxPermSize=256M
-verbose:class 在控制檯打印類加載信息
-verbose:gc 在控制檯打印垃圾回收日誌
-XX:+PrintGC 打印GC日誌,內容簡單
-XX:+PrintGCDetails 打印GC日誌,內容詳細
-XX:+PrintGCDateStamps 在GC日誌中添加時間戳
-Xloggc:filename 指定gc日誌路徑 -Xloggc:/data/jvm/gc.log
-XX:+UseSerialGC 年輕代設置串行收集器Serial
-XX:+UseParallelGC 年輕代設置並行收集器Parallel Scavenge
-XX:ParallelGCThreads=n 設置Parallel Scavenge收集時使用的CPU數。並行收集線程數。 -XX:ParallelGCThreads=4
-XX:MaxGCPauseMillis=n 設置Parallel Scavenge回收的最大時間(毫秒) -XX:MaxGCPauseMillis=100
-XX:GCTimeRatio=n 設置Parallel Scavenge垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n) -XX:GCTimeRatio=19
-XX:+UseParallelOldGC 設置老年代爲並行收集器ParallelOld收集器
-XX:+UseConcMarkSweepGC 設置老年代併發收集器CMS
-XX:+CMSIncrementalMode 設置CMS收集器爲增量模式,適用於單CPU狀況。



鋒哥最新SpringCloud分佈式電商秒殺課程發佈

👇👇👇

   
👆長按上方微信二維碼 2 秒






感謝點贊支持下哈 

本文分享自微信公衆號 - java1234(gh_27ed55ecb177)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索