JVM記憶體佔用情況深入分析,分分鐘解開你的疑惑
很多同學都問過這個問題,為什麼我的Xmx設定4g,但是TOP命令查詢RES卻佔用5G,6G,甚至10G。這個正常嗎?也可以說正常,也可以說不正常,怎麼判斷?筆者今天就要為你解答這個問題,叫你如何分析JVM佔用的記憶體都分配到了哪裡,哪些地方合理,哪些地方異常。
記憶體分佈
首先,列舉一下一個JVM程序主要佔用記憶體的一些地方:
- Young
- Old
- metaspace
- java thread count * Xss
- other thread count * stacksize (非Java執行緒)
- Direct memory
- native memory
- codecache
說明:包括但不限於此。
接下來一步一步驗證每個區域佔用的記憶體。並且為了驗證這個問題,寫了一個工具類,裡面有給每個區域分配記憶體的方法,原始碼在文末。
- JVM引數
執行過程中的JVM引數如下:
-verbose:gc -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn1g -XX:PretenureSizeThreshold=2M -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=90 -XX:+UseCMSInitiatingOccupancyOnly -XX:MaxDirectMemorySize=512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
Young+Old
我們先從最簡單的堆佔用記憶體開始,即Xmx和Xms引數申明,它包括young和old區。分別分配800M和200M記憶體,main方法如下:
public static void main(String[] args) throws Exception{
youngAllocate(800);
oldAllocate(200);
Thread.sleep(300000);
}
通過TOP命令檢視,RES為1G:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 22481 afei 20 0 4366m 1.0g 11m S 0.5 27.0 0:02.41 java
通過jstat命令也能看到,Old和Eden分別佔用200M和800M。
這裡再增加一個有趣的測試,young和old區分別分配1000M和1000M記憶體,main方法如下:
public static void main(String[] args) throws Exception{
youngAllocate(1000);
oldAllocate(1000);
// 為了CMS GC順利觸發,這裡需要sleep 5s以上,建議時間長一點,讓整個CMS GC順利完成。
Thread.sleep(300000);
}
這樣就會導致發生一次YGC和一個CMS GC,那麼你認為這時候通過TOP命令檢視RES結果是多少呢?這時候應該是1.8G,除了S0/S1兩個區域,eden和Old區域都寫入過資料,而JVM使用過的記憶體就不會歸還給作業系統,除非JVM程序宕機或者重啟,這個結論很重要:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22707 afei 20 0 4366m 1.8g 11m S 0.0 48.7 0:00.90 java
Young+Old+Metaspace
接下來,我們再通過程式在Metaspace中重複載入20w個物件,即metaspace分配200M左右的記憶體,main方法如下:
public static void main(String[] args) throws Exception{
youngAllocate(1000);
oldAllocate(1000);
metaspaceAllocate(200000);
Thread.sleep(60000);
}
通過TOP命令檢視,RES為2.0G:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22781 afei 20 0 4472m 2.0g 12m S 0.0 54.7 0:07.51 java
即前面分析的1.8G+208M(213822/1024),在JVM程序退出時有一行這樣的日誌:
Metaspace used 213822K, capacity 215618K, committed 215936K, reserved 1165312K
Young+Old+Metaspace+DirectMemory
接下來,我們再通過程式給堆外分配400M,main方法如下:
public static void main(String[] args) throws Exception{
youngAllocate(1000);
oldAllocate(1000);
metaspaceAllocate(200000);
directMemoryAllocate(400);
Thread.sleep(60000);
}
通過TOP命令檢視,RES為2.4G:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
23329 afei 20 0 4874m 2.4g 12m S 0.0 65.2 0:12.67 java
Abount DirectMemory
在Java的上下文裡,特指通過一組特定的API訪問native memory,這組API主要由DirectByteBuffer暴露出來,其底層是通過c的malloc分配記憶體,API參考:ByteBuffer.allocateDirect(1024),可以通過MaxDirectMemory限制分配上限。
這部分分配的記憶體可以通過VisualVM的MBeans檢視,但是MBeans預設沒有安裝,需要我們自己安裝。但是由於VisualVM的MBeans預設從https://github.com/visualvm/visualvm.src/releases/download/1.3.9/com-sun-tools-visualvm-modules-mbeans.nbm中下載visualvm外掛,而這個路徑已經不存在。所以建議去https://github.com/oracle/visualvm/releases上下載對應的版本,然後手動安裝這個外掛:工具-外掛-已下載-新增外掛,選擇本地已經下載的外掛,最後點選安裝即可。筆者的JDK8預設下載1.3.9版本,那麼就去github上下載1.3.9版本,只需要MBeans這個模組即可:
通過MBeans檢視Direct Memory佔用記憶體非常方便:
Young+Old+Metaspace+DirectMemory+執行緒棧
最後就是執行緒棧,筆者試圖通過啟動20個執行緒,並且設定-Xss10240k,但是並沒有達到預期,這裡作為一個遺留問題。等筆者哪天搞懂了,再發文說明。
- Xss案例
曾經群裡有一個朋友就是因為Xss配置相當大導致RES佔用13G左右。大概情況是這樣,-Xms4g,-Xss40940k,dubbo的provider服務。熟悉dubbo服務同學知道,dubbo服務provider預設採用固定200個執行緒處理的方式。所以200個執行緒佔用8G,加上4G堆,以及一些其他記憶體,導致RSS高達13G,恐怖!!!
codecache
這部分記憶體一般佔用比較少,在JVM崩潰的檔案hs_err_pid18480.log中有其記憶體佔用情況:
CodeCache: size=245760Kb used=47868Kb max_used=47874Kb free=197891Kb
bounds [0x00007f00b4de4000, 0x00007f00b7d54000, 0x00007f00c3de4000]
total_blobs=12973 nmethods=12383 adapters=500
compilation: enabled
知識總結
HotSpot VM自己在JIT編譯器、GC工作等的一些時候都會額外臨時分配一些native memory,在JDK類庫也有可能會有些功能分配長期存活或者臨時的native memory,然後就是各種第三方庫的native部分可能分配的native memory。
總之,RES佔比異常時,一一排查,不要忽略任何一部分可能消耗的記憶體。
jvm使用了的記憶體,即使GC後也不會還給作業系統。
Direct Memory記憶體檢視:如果是JDK 7及以上版本,可以用jconsole或者VisualVM的MBeans視窗檢視java.nio.BufferPool.direct屬性。
文末福利
最後筆者推薦一個JVM引數-XX:NativeMemoryTracking==[off|summary|detail],可以窺探一些我們平常不怎麼關注的記憶體佔用部分,配置JVM引數後,執行如下命令即可:
jcmd 23448 VM.native_memory summary
命令執行結果如下:
測試原始碼
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
/**
* 每個方法的引數m都是表示對應區間分配多少M記憶體
* @author afei
* @date 2018-09-28
* @since 1.0.0
*/
public class MemoryTest {
private static final int _1m = 1024*1024;
private static final long THREAD_SLEEP_MS = 10*1000;
public static void main(String[] args) throws Exception{
youngAllocate(1000);
oldAllocate(1000);
metaspaceAllocate(200000);
directMemoryAllocate(400);
// threadStackAllocate(400);
Thread.sleep(60000);
}
/**
* @param count 重複定義的MyCalc物件數量
*/
private static void metaspaceAllocate(int count) throws Exception {
System.out.println("metaspace object count: " + count);
Method declaredMethod = ClassLoader.class.getDeclaredMethod("defineClass",
new Class[]{String.class, byte[].class, int.class, int.class});
declaredMethod.setAccessible(true);
File classFile = new File("/app/afei/MyCalc.class");
byte[] bcs = new byte[(int) classFile.length()];
try(InputStream is = new FileInputStream(classFile);){
// 將檔案流讀進byte陣列
while (is.read(bcs)!=-1){
}
}
int outputCount = count/10;
for (int i=1; i<=count; i++){
try {
// 重複定義MyCalc這個類
declaredMethod.invoke(
MemoryTest.class.getClassLoader(),
new Object[]{"MyCalc", bcs, 0, bcs.length});
}catch (Throwable e){
// 重複定義類會丟擲LinkageError: attempted duplicate class definition for name: "MyCalc"
// System.err.println(e.getCause().getLocalizedMessage());
}
if (i>=outputCount && i%outputCount==0){
System.out.println("i = "+i);
}
}
System.out.println("metaspace end");
}
/**
* @param m 分配多少M direct memory
*/
private static void directMemoryAllocate(int m){
System.out.println("direct memory: "+m+"m");
for (int i = 0; i < m; i++) {
ByteBuffer.allocateDirect(_1m);
}
System.out.println("direct memory end");
}
/**
* @param m 給young區分配多少M的資料
*/
private static void youngAllocate(int m){
System.out.println("young: "+m+"m");
for (int i = 0; i < m; i++) {
byte[] test = new byte[_1m];
}
System.out.println("young end");
}
/**
* 需要配置引數: -XX:PretenureSizeThreshold=2M, 並且結合CMS
* @param m 給old區分配多少M的資料
*/
private static void oldAllocate(int m){
System.out.println("old: "+m+"m");
for (int i = 0; i < m/5; i++) {
byte[] test = new byte[5*_1m];
}
System.out.println("old end");
}
// 需要配置引數: -Xss10240k, 這裡的實驗以失敗告終
private static void threadStackAllocate(int m){
int threadCount = m/10;
System.out.println("thread stack count:"+threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
System.out.println("thread name: " + Thread.currentThread().getName());
try {
while(true) {
Thread.sleep(THREAD_SLEEP_MS);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
System.out.println("thread stack end:"+threadCount);
}
}
作者:佔小狼
連結:https://www.jianshu.com/p/0cbc4e44c596
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。