1. 程式人生 > >JVM記憶體佔用情況深入分析,分分鐘解開你的疑惑

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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。