JVM 調(diào)優(yōu)工具詳解及調(diào)優(yōu)實(shí)戰(zhàn))
前置說明事先啟動(dòng)一個(gè) Web 應(yīng)用程序用jps查看其進(jìn)程 id接著用各種 JDK 自帶命令優(yōu)化應(yīng)用。Jmapjmap命令可以用來查看內(nèi)存信息實(shí)例個(gè)數(shù)以及占用內(nèi)存大小。打開log.txt文件內(nèi)容如下num序號instances實(shí)例數(shù)量bytes占用空間大小class name類名稱[Cis achar[][Sis ashort[][Iis aint[][Bis abyte[][[Iis aint[][]二維 int 數(shù)組堆信息堆內(nèi)存 dumpjmap-dump:formatb,fileeureka.hprof14660也可以設(shè)置內(nèi)存溢出自動(dòng)導(dǎo)出 dump 文件內(nèi)存很大的時(shí)候可能會導(dǎo)不出來-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath./# 路徑示例代碼publicclassOOMTest{// JVM 設(shè)置// -Xms10M -Xmx10M -XX:PrintGCDetails -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPathD:\jvm.dumppublicstaticListObjectlistnewArrayList();publicstaticvoidmain(String[]args){ListObjectlistnewArrayList();inti0;intj0;while(true){list.add(newUser(i,UUID.randomUUID().toString()));newUser(j--,UUID.randomUUID().toString());}}}可以用jvisualvm命令工具導(dǎo)入該 dump 文件分析Jstack用jstack加進(jìn)程 id 查找死鎖見如下示例。死鎖示例DeadLockTestpublicclassDeadLockTest{privatestaticObjectlock1newObject();privatestaticObjectlock2newObject();publicstaticvoidmain(String[]args){newThread(()-{synchronized(lock1){try{System.out.println(thread1 begin);Thread.sleep(5000);}catch(InterruptedExceptione){}synchronized(lock2){System.out.println(thread1 end);}}}).start();newThread(()-{synchronized(lock2){try{System.out.println(thread2 begin);Thread.sleep(5000);}catch(InterruptedExceptione){}synchronized(lock1){System.out.println(thread2 end);}}}).start();System.out.println(main thread end);}}jstack輸出中的關(guān)鍵信息含義Thread-1線程名prio5優(yōu)先級 5tid0x000000001fa9e000線程 idnid0x2d64線程對應(yīng)的本地線程標(biāo)識 nidjava.lang.Thread.State: BLOCKED線程狀態(tài)還可以用jvisualvm自動(dòng)檢測死鎖找出占用 CPU 最高的線程堆棧信息packagecom.tuling.jvm;/** * 運(yùn)行此代碼cpu 會飆高 */publicclassMath{publicstaticfinalintinitData666;publicstaticUserusernewUser();publicintcompute(){// 一個(gè)方法對應(yīng)一塊棧幀內(nèi)存區(qū)域inta1;intb2;intc(ab)*10;returnc;}publicstaticvoidmain(String[]args){MathmathnewMath();while(true){math.compute();}}}排查步驟使用命令top -p pid顯示你的 Java 進(jìn)程的內(nèi)存情況pid 是你的 Java 進(jìn)程號比如19663。按H獲取每個(gè)線程的內(nèi)存情況。找到內(nèi)存和 CPU 占用最高的線程 tid比如19664。轉(zhuǎn)為十六進(jìn)制得到0x4cd0此為線程 id 的十六進(jìn)制表示。執(zhí)行jstack 19663 | grep -A 10 4cd0得到線程堆棧信息中4cd0這個(gè)線程所在行的后面 10 行從堆棧中可以發(fā)現(xiàn)導(dǎo)致 CPU 飆高的調(diào)用方法。查看對應(yīng)的堆棧信息找出可能存在問題的代碼。遠(yuǎn)程連接 jvisualvm啟動(dòng)普通的 jar 程序 JMX 端口配置java\-Dcom.sun.management.jmxremote.port8888\-Djava.rmi.server.hostname192.168.50.60\-Dcom.sun.management.jmxremote.sslfalse\-Dcom.sun.management.jmxremote.authenticatefalse\-jaryour-app.jarPS-Dcom.sun.management.jmxremote.port為遠(yuǎn)程機(jī)器的 JMX 端口-Djava.rmi.server.hostname為遠(yuǎn)程機(jī)器 IPTomcat 的 JMX 配置在catalina.sh文件里最后一個(gè)JAVA_OPTS的賦值語句下一行增加如下配置行JAVA_OPTS$JAVA_OPTS-Dcom.sun.management.jmxremote.port8888 -Djava.rmi.server.hostname192.168.50.60 -Dcom.sun.management.jmxremote.sslfalse -Dcom.sun.management.jmxremote.authenticatefalseJinfo查看正在運(yùn)行的 Java 應(yīng)用程序的擴(kuò)展參數(shù)查看 JVM 的參數(shù)查看 Java 系統(tǒng)參數(shù)Jstatjstat命令可以查看堆內(nèi)存各部分的使用量以及加載類的數(shù)量。命令的格式如下jstat[-命令選項(xiàng)][vmid][間隔時(shí)間(毫秒)][查詢次數(shù)]注意使用的 JDK 版本是 JDK 8。垃圾回收統(tǒng)計(jì)jstat -gc pid最常用可以評估程序內(nèi)存使用及 GC 壓力整體情況。S0C第一個(gè)幸存區(qū)的大小單位 KBS1C第二個(gè)幸存區(qū)的大小S0U第一個(gè)幸存區(qū)的使用大小S1U第二個(gè)幸存區(qū)的使用大小EC伊甸園區(qū)的大小EU伊甸園區(qū)的使用大小OC老年代大小OU老年代使用大小MC方法區(qū)大小元空間MU方法區(qū)使用大小CCSC壓縮類空間大小CCSU壓縮類空間使用大小YGC年輕代垃圾回收次數(shù)YGCT年輕代垃圾回收消耗時(shí)間單位 sFGC老年代垃圾回收次數(shù)FGCT老年代垃圾回收消耗時(shí)間單位 sGCT垃圾回收消耗總時(shí)間單位 s堆內(nèi)存統(tǒng)計(jì)NGCMN新生代最小容量NGCMX新生代最大容量NGC當(dāng)前新生代容量S0C第一個(gè)幸存區(qū)大小S1C第二個(gè)幸存區(qū)的大小EC伊甸園區(qū)的大小OGCMN老年代最小容量OGCMX老年代最大容量OGC當(dāng)前老年代大小OC當(dāng)前老年代大小MCMN最小元數(shù)據(jù)容量MCMX最大元數(shù)據(jù)容量MC當(dāng)前元數(shù)據(jù)空間大小CCSMN最小壓縮類空間大小CCSMX最大壓縮類空間大小CCSC當(dāng)前壓縮類空間大小YGC年輕代 GC 次數(shù)FGC老年代 GC 次數(shù)新生代垃圾回收統(tǒng)計(jì)S0C第一個(gè)幸存區(qū)的大小S1C第二個(gè)幸存區(qū)的大小S0U第一個(gè)幸存區(qū)的使用大小S1U第二個(gè)幸存區(qū)的使用大小TT對象在新生代存活的次數(shù)MTT對象在新生代存活的最大次數(shù)DSS期望的幸存區(qū)大小EC伊甸園區(qū)的大小EU伊甸園區(qū)的使用大小YGC年輕代垃圾回收次數(shù)YGCT年輕代垃圾回收消耗時(shí)間新生代內(nèi)存統(tǒng)計(jì)NGCMN新生代最小容量NGCMX新生代最大容量NGC當(dāng)前新生代容量S0CMX最大幸存 1 區(qū)大小S0C當(dāng)前幸存 1 區(qū)大小S1CMX最大幸存 2 區(qū)大小S1C當(dāng)前幸存 2 區(qū)大小ECMX最大伊甸園區(qū)大小EC當(dāng)前伊甸園區(qū)大小YGC年輕代垃圾回收次數(shù)FGC老年代回收次數(shù)老年代垃圾回收統(tǒng)計(jì)MC方法區(qū)大小MU方法區(qū)使用大小CCSC壓縮類空間大小CCSU壓縮類空間使用大小OC老年代大小OU老年代使用大小YGC年輕代垃圾回收次數(shù)FGC老年代垃圾回收次數(shù)FGCT老年代垃圾回收消耗時(shí)間GCT垃圾回收消耗總時(shí)間老年代內(nèi)存統(tǒng)計(jì)OGCMN老年代最小容量OGCMX老年代最大容量OGC當(dāng)前老年代大小OC老年代大小YGC年輕代垃圾回收次數(shù)FGC老年代垃圾回收次數(shù)FGCT老年代垃圾回收消耗時(shí)間GCT垃圾回收消耗總時(shí)間元數(shù)據(jù)空間統(tǒng)計(jì)MCMN最小元數(shù)據(jù)容量MCMX最大元數(shù)據(jù)容量MC當(dāng)前元數(shù)據(jù)空間大小CCSMN最小壓縮類空間大小CCSMX最大壓縮類空間大小CCSC當(dāng)前壓縮類空間大小YGC年輕代垃圾回收次數(shù)FGC老年代垃圾回收次數(shù)FGCT老年代垃圾回收消耗時(shí)間GCT垃圾回收消耗總時(shí)間各區(qū)域使用比例summaryS0幸存 1 區(qū)當(dāng)前使用比例S1幸存 2 區(qū)當(dāng)前使用比例E伊甸園區(qū)使用比例O老年代使用比例M元數(shù)據(jù)區(qū)使用比例CCS壓縮使用比例YGC年輕代垃圾回收次數(shù)FGC老年代垃圾回收次數(shù)FGCT老年代垃圾回收消耗時(shí)間GCT垃圾回收消耗總時(shí)間JVM 運(yùn)行情況預(yù)估用jstat gc -pid命令可以計(jì)算出如下一些關(guān)鍵數(shù)據(jù)有了這些數(shù)據(jù)就可以采用之前介紹過的優(yōu)化思路先給自己的系統(tǒng)設(shè)置一些初始性的 JVM 參數(shù)比如堆內(nèi)存大小、年輕代大小、Eden 和 Survivor 的比例、老年代的大小、大對象的閾值、大齡對象進(jìn)入老年代的閾值等。年輕代對象增長的速率可以執(zhí)行命令jstat -gc pid 1000 10每隔 1 秒執(zhí)行 1 次命令共執(zhí)行 10 次通過觀察 EUEden 區(qū)的使用來估算每秒 Eden 大概新增多少對象。如果系統(tǒng)負(fù)載不高可以把頻率 1 秒換成 1 分鐘甚至 10 分鐘來觀察整體情況。注意一般系統(tǒng)可能有高峰期和日常期所以需要在不同的時(shí)間分別估算不同情況下對象增長速率。Young GC 的觸發(fā)頻率和每次耗時(shí)知道年輕代對象增長速率我們就能根據(jù) Eden 區(qū)的大小推算出 Young GC 大概多久觸發(fā)一次Young GC 的平均耗時(shí)可以通過YGCT / YGC公式算出。根據(jù)結(jié)果我們大概就能知道系統(tǒng)大概多久會因?yàn)?Young GC 的執(zhí)行而卡頓多久。每次 Young GC 后有多少對象存活和進(jìn)入老年代假設(shè)已經(jīng)大概知道 Young GC 的頻率比如每 5 分鐘一次那么可以執(zhí)行命令jstat -gc pid 300000 10觀察每次結(jié)果 Eden、Survivor 和老年代使用的變化情況。在每次 GC 后 Eden 區(qū)使用一般會大幅減少Survivor 和老年代都有可能增長這些增長的對象就是每次 Young GC 后存活的對象同時(shí)還可以看出每次 Young GC 后進(jìn)入老年代大概多少對象從而可以推算出老年代對象增長速率。Full GC 的觸發(fā)頻率和每次耗時(shí)知道了老年代對象的增長速率就可以推算出 Full GC 的觸發(fā)頻率了Full GC 的每次耗時(shí)可以用公式FGCT / FGC計(jì)算得出。優(yōu)化思路簡單來說就是盡量讓每次 Young GC 后的存活對象小于 Survivor 區(qū)域的 50%都留存在年輕代里盡量別讓對象進(jìn)入老年代盡量減少 Full GC 的頻率避免頻繁 Full GC 對 JVM 性能的影響。系統(tǒng)頻繁 Full GC 導(dǎo)致系統(tǒng)卡頓是怎么回事機(jī)器配置2 核 4GJVM 內(nèi)存大小2G系統(tǒng)運(yùn)行時(shí)間7 天期間發(fā)生的 Full GC 次數(shù)和耗時(shí)500 多次200 多秒期間發(fā)生的 Young GC 次數(shù)和耗時(shí)1 萬多次500 多秒大致算下來每天會發(fā)生 70 多次 Full GC平均每小時(shí) 3 次每次 Full GC 在 400 毫秒左右每天會發(fā)生 1000 多次 Young GC每分鐘會發(fā)生 1 次每次 Young GC 在 50 毫秒左右。JVM 參數(shù)設(shè)置如下-Xms1536M-Xmx1536M-Xmn512M-Xss256K-XX:SurvivorRatio6-XX:MetaspaceSize256M-XX:MaxMetaspaceSize256M-XX:UseParNewGC-XX:UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction75-XX:UseCMSInitiatingOccupancyOnly大家可以結(jié)合「對象挪動(dòng)到老年代那些規(guī)則」推理下我們這個(gè)程序可能存在的一些問題。經(jīng)過分析感覺可能會由于對象動(dòng)態(tài)年齡判斷機(jī)制導(dǎo)致 Full GC 較為頻繁。為了給大家看效果我模擬了一個(gè)示例程序見課程對應(yīng)工程代碼jvm-full-gc打印了jstat的結(jié)果如下jstat-gc13456200010000對于對象動(dòng)態(tài)年齡判斷機(jī)制導(dǎo)致的 Full GC 較為頻繁可以先試著優(yōu)化下 JVM 參數(shù)把年輕代適當(dāng)調(diào)大點(diǎn)-Xms1536M-Xmx1536M-Xmn1024M-Xss256K-XX:SurvivorRatio6-XX:MetaspaceSize256M-XX:MaxMetaspaceSize256M-XX:UseParNewGC-XX:UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction92-XX:UseCMSInitiatingOccupancyOnly優(yōu)化完發(fā)現(xiàn)沒什么變化Full GC 的次數(shù)比 Minor GC 的次數(shù)還多了我們可以推測下 Full GC 比 Minor GC 還多的原因有哪些元空間不夠?qū)е碌亩嘤?Full GC顯式調(diào)用System.gc()造成多余的 Full GC這種一般線上盡量通過-XX:DisableExplicitGC參數(shù)禁用如果加上了這個(gè) JVM 啟動(dòng)參數(shù)那么代碼中調(diào)用System.gc()沒有任何效果老年代空間分配擔(dān)保機(jī)制。最快速度分析完這些我們推測的原因以及優(yōu)化后我們發(fā)現(xiàn) Young GC 和 Full GC 依然很頻繁了而且看到有大量的對象頻繁地被挪動(dòng)到老年代。這種情況我們可以借助jmap命令大概看下是什么對象查到了有大量User對象產(chǎn)生這個(gè)可能是問題所在但不確定還必須找到對應(yīng)的代碼確認(rèn)。如何去找對應(yīng)的代碼代碼里全文搜索生成User對象的地方適合只有少數(shù)幾處地方的情況如果生成User對象的地方太多無法定位具體代碼我們可以同時(shí)分析下占用 CPU 較高的線程。一般有大量對象不斷產(chǎn)生對應(yīng)的方法代碼肯定會被頻繁調(diào)用占用的 CPU 必然較高??梢杂蒙厦嬷v過的jstack或jvisualvm來定位 CPU 使用較高的代碼最終定位到的代碼如下importjava.util.ArrayList;importjava.util.List;RestControllerpublicclassIndexController{RequestMapping(/user/process)publicStringprocessUserData()throwsInterruptedException{// 一次查詢出大量示例約 500MUser 對象未做分頁/限制ListUserusersqueryUsers();for(Useruser:users){// 業(yè)務(wù)處理...對象朝生夕死卻大量進(jìn)入老年代}returnsuccess;}}注以上IndexController為根據(jù)原文可見片段及上下文重建的示意代碼原導(dǎo)出文檔此處被截?cái)鄡H保留了import、類定義與queryUsers()調(diào)用部分。同時(shí)Java 的代碼也是需要優(yōu)化的一次查詢出 500M 的對象出來明顯不合適。要根據(jù)之前說的各種原則盡量優(yōu)化到合適的值盡量消除這種朝生夕死的對象導(dǎo)致的 Full GC。內(nèi)存泄漏再給大家講一種情況一般電商架構(gòu)可能會使用多級緩存架構(gòu)就是 Redis 加上 JVM 級緩存。大多數(shù)同學(xué)可能為了圖方便對于 JVM 級緩存就簡單使用一個(gè)HashMap于是不斷往里面放緩存數(shù)據(jù)但是很少考慮這個(gè) map 的容量問題結(jié)果這個(gè)緩存 map 越來越大一直占用著老年代的很多空間時(shí)間長了就會導(dǎo)致 Full GC 非常頻繁——這就是一種內(nèi)存泄漏對于一些老舊數(shù)據(jù)沒有及時(shí)清理導(dǎo)致一直占用著寶貴的內(nèi)存資源。時(shí)間長了除了導(dǎo)致 Full GC還有可能導(dǎo)致 OOM。這種情況完全可以考慮采用一些成熟的 JVM 級緩存框架來解決比如 Ehcache 等自帶一些 LRU 數(shù)據(jù)淘汰算法的框架來作為 JVM 級的緩存。參考鏈接http://note.youdao.com/noteshare?id5cc182642eb02bc64197788c7722baaesubE333076E3DCC4A6C9E01CA6BD05DD6A0