前置啟動程序
事先啟動一個web應(yīng)用程序,用jps查看其進(jìn)程id,接著用各種jdk自帶命令優(yōu)化應(yīng)用
Jmap
此命令可以用來查看內(nèi)存信息,實例個數(shù)以及占用內(nèi)存大小
jmap -histo 14052 #查看歷史生成的實例
jmap -histo:live 14052#查看當(dāng)前存活的實例,執(zhí)行過程中可能會觸發(fā)一次full gc
打開log.txt,文件內(nèi)容如下:
- num:序號
- instances:實例數(shù)量
- bytes:占用空間大小
- class name:類名稱,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
堆信息
堆內(nèi)存dump
也可以設(shè)置內(nèi)存溢出自動導(dǎo)出dump文件(內(nèi)存很大的時候,可能會導(dǎo)不出來)
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./ (路徑)
可以用jvisualvm命令工具導(dǎo)入該dump文件分析
Jstack
用jstack加進(jìn)程id查找死鎖,見如下示例
package user_portrait;
/**
* @program: portrait
* @description: DeadLockTest
* @author: yxh-word
* @create: 2021-08-11
* @version: v1.0.0 創(chuàng)建文件, yxh-word, 2021-08-11
**/
public class DeadLockTest {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
try {
System.out.println("thread1 begin");
Thread.sleep(5000);
} catch (InterruptedException e) {
}
synchronized (lock2) {
System.out.println("thread1 end");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try {
System.out.println("thread2 begin");
Thread.sleep(5000);
} catch (InterruptedException e) {
}
synchronized (lock1) {
System.out.println("thread2 end");
}
}
}).start();
System.out.println("main thread end");
}
}
"Thread-1" 線程名
prio=5 優(yōu)先級=5
tid=0x0000000021428000 線程id
nid=0x3eb8 線程對應(yīng)的本地線程標(biāo)識nid
JAVA.lang.Thread.State: BLOCKED 線程狀態(tài)
還可以用jvisualvm自動檢測死鎖
jstack找出占用cpu最高的線程堆棧信息
1,使用命令top -p ,顯示你的java進(jìn)程的內(nèi)存情況,pid是你的java進(jìn)程號,比如19663
2,按H,獲取每個線程的內(nèi)存情況
3,找到內(nèi)存和cpu占用最高的線程tid,比如19664
4,轉(zhuǎn)為十六進(jìn)制得到 0x4cd0,此為線程id的十六進(jìn)制表示
5,執(zhí)行 jstack 19663|grep -A 10 4cd0,得到線程堆棧信息中 4cd0 這個線程所在行的后面10行,從堆棧中可以發(fā)現(xiàn)導(dǎo)致cpu飆高的調(diào)用方法
6,查看對應(yīng)的堆棧信息找出可能存在問題的代碼
Jinfo
查看正在運行的Java應(yīng)用程序的擴(kuò)展參數(shù)
查看jvm的參數(shù)
查看java系統(tǒng)參數(shù)
Jstat
jstat命令可以查看堆內(nèi)存各部分的使用量,以及加載類的數(shù)量。命令的格式如下:
jstat [-命令選項] [vmid] [間隔時間(毫秒)] [查詢次數(shù)]
注意:使用的jdk版本是jdk8
垃圾回收統(tǒng)計
jstat -gc pid 最常用,可以評估程序內(nèi)存使用及GC壓力整體情況
- S0C:第一個幸存區(qū)的大小,單位KB
- S1C:第二個幸存區(qū)的大小
- S0U:第一個幸存區(qū)的使用大小
- S1U:第二個幸存區(qū)的使用大小
- EC:伊甸園區(qū)的大小
- EU:伊甸園區(qū)的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法區(qū)大小(元空間)
- MU:方法區(qū)使用大小
- CCSC:壓縮類空間大小
- CCSU:壓縮類空間使用大小
- YGC:年輕代垃圾回收次數(shù)
- YGCT:年輕代垃圾回收消耗時間,單位s
- FGC:老年代垃圾回收次數(shù)
- FGCT:老年代垃圾回收消耗時間,單位s
- GCT:垃圾回收消耗總時間,單位s
堆內(nèi)存統(tǒng)計
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:當(dāng)前新生代容量
- S0C:第一個幸存區(qū)大小
- S1C:第二個幸存區(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)計
- S0C:第一個幸存區(qū)的大小
- S1C:第二個幸存區(qū)的大小
- S0U:第一個幸存區(qū)的使用大小
- S1U:第二個幸存區(qū)的使用大小
- TT:對象在新生代存活的次數(shù)
- MTT:對象在新生代存活的最大次數(shù)
- DSS:期望的幸存區(qū)大小
- EC:伊甸園區(qū)的大小
- EU:伊甸園區(qū)的使用大小
- YGC:年輕代垃圾回收次數(shù)
- YGCT:年輕代垃圾回收消耗時間
新生代內(nèi)存統(tǒng)計
- 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)計
- MC:方法區(qū)大小
- MU:方法區(qū)使用大小
- CCSC:壓縮類空間大小
- CCSU:壓縮類空間使用大小
- OC:老年代大小
- OU:老年代使用大小
- YGC:年輕代垃圾回收次數(shù)
- FGC:老年代垃圾回收次數(shù)
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
老年代內(nèi)存統(tǒng)計
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:當(dāng)前老年代大小
- OC:老年代大小
- YGC:年輕代垃圾回收次數(shù)
- FGC:老年代垃圾回收次數(shù)
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
元數(shù)據(jù)空間統(tǒng)計
- MCMN:最小元數(shù)據(jù)容量
- MCMX:最大元數(shù)據(jù)容量
- MC:當(dāng)前元數(shù)據(jù)空間大小
- CCSMN:最小壓縮類空間大小
- CCSMX:最大壓縮類空間大小
- CCSC:當(dāng)前壓縮類空間大小
- YGC:年輕代垃圾回收次數(shù)
- FGC:老年代垃圾回收次數(shù)
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
- S0:幸存1區(qū)當(dāng)前使用比例
- S1:幸存2區(qū)當(dāng)前使用比例
- E:伊甸園區(qū)使用比例
- O:老年代使用比例
- M:元數(shù)據(jù)區(qū)使用比例
- CCS:壓縮使用比例
- YGC:年輕代垃圾回收次數(shù)
- FGC:老年代垃圾回收次數(shù)
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
JVM運行情況預(yù)估
用 jstat gc -pid 命令可以計算出如下一些關(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次),通過觀察EU(eden區(qū)的使用)來估算每秒eden大概新增多少對象,如果系統(tǒng)負(fù)載不高,可以把頻率1秒換成1分鐘,甚至10分鐘來觀察整體情況。注意,一般系統(tǒng)可能有高峰期和日常期,所以需要在不同的時間分別估算不同情況下對象增長速率。
Young GC的觸發(fā)頻率和每次耗時
知道年輕代對象增長速率我們就能推根據(jù)eden區(qū)的大小推算出Young GC大概多久觸發(fā)一次,Young GC的平均耗時可以通過 YGCT/YGC 公式算出,根據(jù)結(jié)果我們大概就能知道系統(tǒng)大概多久會因為Young GC的執(zhí)行而卡頓多久。
每次Young GC后有多少對象存活和進(jìn)入老年代
這個因為之前已經(jīng)大概知道Young GC的頻率,假設(shè)是每5分鐘一次,那么可以執(zhí)行命令 jstat -gc pid 300000 10 ,觀察每次結(jié)果eden,survivor和老年代使用的變化情況,在每次gc后eden區(qū)使用一般會大幅減少,survivor和老年代都有可能增長,這些增長的對象就是每次Young GC后存活的對象,同時還可以看出每次Young GC后進(jìn)去老年代大概多少對象,從而可以推算出老年代對象增長速率。
Full GC的觸發(fā)頻率和每次耗時
知道了老年代對象的增長速率就可以推算出Full GC的觸發(fā)頻率了,F(xiàn)ull GC的每次耗時可以用公式 FGCT/FGC 計算得出。
優(yōu)化思路其實簡單來說就是盡量讓每次Young GC后的存活對象小于Survivor區(qū)域的50%,都留存在年輕代里。盡量別讓對象進(jìn)入老年代。盡量減少Full GC的頻率,避免頻繁Full GC對JVM性能的影響。
系統(tǒng)頻繁Full GC導(dǎo)致系統(tǒng)卡頓是怎么回事
- 機(jī)器配置:2核4G
- JVM內(nèi)存大小:2G
- 系統(tǒng)運行時間:7天
- 期間發(fā)生的Full GC次數(shù)和耗時:500多次,200多秒
- 期間發(fā)生的Young GC次數(shù)和耗時:1萬多次,500多秒
大致算下來每天會發(fā)生70多次Full GC,平均每小時3次,每次Full GC在400毫秒左右;
每天會發(fā)生1000多次Young GC,每分鐘會發(fā)生1次,每次Young GC在50毫秒左右。
JVM參數(shù)設(shè)置如下:
-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
大家可以結(jié)合對象挪動到老年代那些規(guī)則推理下我們這個程序可能存在的一些問題
經(jīng)過分析感覺可能會由于對象動態(tài)年齡判斷機(jī)制導(dǎo)致full gc較為頻繁
為了給大家看效果,我模擬了一個示例程序(見課程對應(yīng)工程代碼:jvm-full-gc),打印了jstat的結(jié)果如下:
jstat -gc 13456 2000 10000
對于對象動態(tài)年齡判斷機(jī)制導(dǎo)致的full gc較為頻繁可以先試著優(yōu)化下JVM參數(shù),把年輕代適當(dāng)調(diào)大點:
-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
優(yōu)化完發(fā)現(xiàn)沒什么變化,full gc的次數(shù)比minor gc的次數(shù)還多了
我們可以推測下full gc比minor gc還多的原因有哪些?
1、元空間不夠?qū)е碌亩嘤鄁ull gc
2、顯示調(diào)用System.gc()造成多余的full gc,這種一般線上盡量通過-XX:+DisableExplicitGC參數(shù)禁用,如果加上了這個JVM啟動參數(shù),那么代碼中調(diào)用System.gc()沒有任何效果
3、老年代空間分配擔(dān)保機(jī)制
最快速度分析完這些我們推測的原因以及優(yōu)化后,我們發(fā)現(xiàn)young gc和full gc依然很頻繁了,而且看到有大量的對象頻繁的被挪動到老年代,這種情況我們可以借助jmap命令大概看下是什么對象
查到了有大量User對象產(chǎn)生,這個可能是問題所在,但不確定,還必須找到對應(yīng)的代碼確認(rèn),如何去找對應(yīng)的代碼了?
1、代碼里全文搜索生成User對象的地方(適合只有少數(shù)幾處地方的情況)
2、如果生成User對象的地方太多,無法定位具體代碼,我們可以同時分析下占用cpu較高的線程,一般有大量對象不斷產(chǎn)生,對應(yīng)的方法代碼肯定會被頻繁調(diào)用,占用的cpu必然較高
可以用上面講過的jstack或jvisualvm來定位cpu使用較高的代碼,最終定位到
同時,java的代碼也是需要優(yōu)化的,一次查詢出500M的對象出來,明顯不合適,要根據(jù)之前說的各種原則盡量優(yōu)化到合適的值,盡量消除這種朝生夕死的對象導(dǎo)致的full gc
內(nèi)存泄露到底是怎么回事
再給大家講一種情況,一般電商架構(gòu)可能會使用多級緩存架構(gòu),就是redis加上JVM級緩存,大多數(shù)同學(xué)可能為了圖方便對于JVM級緩存就簡單使用一個hashmap,于是不斷往里面放緩存數(shù)據(jù),但是很少考慮這個map的容量問題,結(jié)果這個緩存map越來越大,一直占用著老年代的很多空間,時間長了就會導(dǎo)致full gc非常頻繁,這就是一種內(nèi)存泄漏,對于一些老舊數(shù)據(jù)沒有及時清理導(dǎo)致一直占用著寶貴的內(nèi)存資源,時間長了除了導(dǎo)致full gc,還有可能導(dǎo)致OOM。
這種情況完全可以考慮采用一些成熟的JVM級緩存框架來解決,比如ehcache等自帶一些LRU數(shù)據(jù)淘汰算法的框架來作為JVM級的緩存。






