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=256mYoung+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 javaYoung+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 1165312KYoung+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 javaAbount 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=197891Kbbounds [0x00007f00b4de4000, 0x00007f00b7d54000, 0x00007f00c3de4000]total_blobs=12973 nmethods=12383 adapters=500compilation: 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。
總結
以上是生活随笔為你收集整理的JVM内存占用情况深入分析,分分钟解开你的疑惑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员的创业陷阱:接私活
- 下一篇: 拿什么来衡量程序员的生产力?