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