JVM調優目標:使用較小的記憶體佔用來獲得較高的吞吐量或較低的延遲。
程式在上線前的測試或運行中有時會出現一些大大小小的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 | ##在控制台列印垃圾回收日誌||
#列印GC日誌,內容簡單 | ||
列印GC日誌,內容詳細 | ||
#在GC日誌中新增時間戳 | ||
指定gc日誌路徑 | -Xloggc:/data/jvm/gc .log | |
年輕代設定串列收集器Serial | ||
年輕代設定並行收集器Parallel Scavenge | # | |
#設定Parallel Scavenge收集時所使用的CPU數。並行收集線程數。 | -XX:ParallelGCThreads=4 | |
設定Parallel Scavenge回收的最大時間(毫秒) | -XX:MaxGCPauseMillis=100 | |
設定Parallel Scavenge垃圾回收時間佔程式運作時間的百分比。公式為1/(1 n) | -XX:GCTimeRatio=19 | |
設定老年代為平行收集器ParallelOld收集器 | ||
設定舊年代並發收集器CMS | ||
設定CMS收集器為增量模式,適用於單一CPU情況。 |
java教學欄目,歡迎學習!
以上是為什麼jvm要效能調優?的詳細內容。更多資訊請關注PHP中文網其他相關文章!