[JVM教程與調優] 了解JVM 堆記憶體溢出以及非堆記憶體溢出
- 2020 年 4 月 1 日
- 筆記
在上一章中我們介紹了JVM運行時參數以及jstat指令相關內容:[JVM教程與調優] 什麼是JVM運行時參數?。下面我們來介紹一下jmap+MAT記憶體溢出。
首先我們來介紹一下下JVM的記憶體結構。
JVM記憶體結構介紹
從圖中我們可以看到,JVM
的記憶體結構分為兩大塊。一塊叫堆區,一塊叫非堆區。
堆區又分為兩大塊,一塊Young,一塊叫Old。Young區又分為Survivor區和Eden區。Survivor區我們又分為S0與S1。可以結合下圖進行理解
非堆區呢,是屬於我們作業系統的本地記憶體。它是獨立於我們堆區之外的。它在JDK1.8裡面有一個新的名字,叫Metaspace
。Metaspace
裡面還包含幾個塊,其中有一塊就是CCS
,還有一塊是CodeCache
。當然,在我們的Metaspace中還包含很多其他塊,這裡就不做擴展了。
接下來,我們來通過實戰,來更加深入的理解JVM
結構,以及出現JVM記憶體溢出的原因。
實戰理解
我們通過spring.start快速來生成一個springboot項目。
如圖,我們快速的創建一個springboot項目,並將其下載下來。
這裡我使用Eclipse,小夥伴們也可以使用IDEA或者其他開發工具也是可以的。
這裡我們使用的是SpringBoot工程,如果有的小夥伴對SpringBoot還不太熟悉的,可以上網找一些教程先學習了解一下。
堆記憶體溢出演示
那麼我們如何來構建一個堆記憶體溢出呢?其實很簡單,我們只要定義一個List
對象,然後通過一個循環不停的往List
裡面塞對象。因為只要Controller不被回收,那麼它裡面的成員變數也是不會被回收的。這樣就會導致List裡面的對象越來越多,佔用的記憶體越來越大,最後就把我們的記憶體撐爆了。
創建User對象
這裡我們先創建一個User對象。
/** * * <p>Title: User</p> * <p>Description: </p> * @author Coder編程 * @date 2020年3月29日 */ @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; }
這裡面@Data
、@AllArgsConstructor
、@NoArgsConstructor
用的是lombok註解。不會使用的小夥伴,可以在網上查找相關資料學習一下。
創建Controller對象
接下來我們來創建一個Controller來不停的往List集合中塞對象。
/** * * <p>Title: MemoryController</p> * <p>Description: </p> * @author Coder編程 * @date 2020年3月29日 */ @RestController public class MemoryController { private List<User> userList = new ArrayList<User>(); /** * -Xmx32M -Xms32M * */ @GetMapping("/heap") public String heap() { int i=0; while(true) { userList.add(new User(i++, UUID.randomUUID().toString())); } } }
為了更快達到我們的效果,我們來設置兩個參數。
-Xmx32M -Xms32M
一個最大記憶體,一個最小記憶體。我們的堆就只有32M,這樣就很容易溢出。
訪問測試
啟動時候設置記憶體參數。
記得選中我們的Arguments
,在JVM
參數中,將我們的值設置進去。最後點擊Run
運行起來。
然後我們在瀏覽器中請求:
http://localhost:8080/heap
我們再觀察控制台列印:
通過列印結果,我們可以看到堆記憶體溢出了。
注意:
這裡我們測試的時候可以很簡單的看出在哪裡出現的問題,但是在實際生產環境中並沒有那麼簡單,因此我們需要藉助工具,來定位這些問題。後續我們來介紹一下。
非堆記憶體溢出演示
接下來我們來演示一下非堆記憶體溢出,我們繼續沿用上方程式碼。
非堆記憶體主要是MataSpace,那麼我們如何構建一個非堆記憶體溢出呢?
我們知道MataSpace主要存一些class,filed,method等這些東西。
因此我們繼續創建一個List集合,不斷的往集合裡面塞class。只要List不被回收,那麼它裡面的class也不會被回收。不停的往裡面加之後,就會造成溢出。也就是我們的MataSpace溢出了。
如何來動態生成一些class呢?其實是有很多工具的,比如說:asm
引入asm工具包
這裡我們引入asm jar包。
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency>
動態生成類文件
還需要創建動態生成的類文件,這裡我們就不做擴展介紹,有興趣的小夥伴可以自行到網上查閱。
/** * * <p>Title: Metaspace</p> * <p>Description: https://blog.csdn.net/bolg_hero/article/details/78189621 * 繼承ClassLoader是為了方便調用defineClass方法,因為該方法的定義為protected</p> * @author Coder編程 * @date 2020年3月29日 */ public class Metaspace extends ClassLoader { public static List<Class<?>> createClasses() { // 類持有 List<Class<?>> classes = new ArrayList<Class<?>>(); // 循環1000w次生成1000w個不同的類。 for (int i = 0; i < 10000000; ++i) { ClassWriter cw = new ClassWriter(0); // 定義一個類名稱為Class{i},它的訪問域為public,父類為java.lang.Object,不實現任何介面 cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 定義構造函數<init>方法 MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); // 第一個指令為載入this mw.visitVarInsn(Opcodes.ALOAD, 0); // 第二個指令為調用父類Object的構造函數 mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); // 第三條指令為return mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); Metaspace test = new Metaspace(); byte[] code = cw.toByteArray(); // 定義類 Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } return classes; } }
創建Controller
接下來我們再原Controller
新增一個方法nonheap
/** * * <p>Title: MemoryController</p> * <p>Description: </p> * @author Coder編程 * @date 2020年3月29日 */ @RestController public class MemoryController { private List<User> userList = new ArrayList<User>(); private List<Class<?>> classList = new ArrayList<Class<?>>(); /** * -Xmx32M -Xms32M * */ @GetMapping("/heap") public String heap() { int i=0; while(true) { userList.add(new User(i++, UUID.randomUUID().toString())); } } /** * -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M * */ @GetMapping("/nonheap") public String nonheap() { while(true) { classList.addAll(Metaspace.createClasses()); } } }
訪問測試
這裡我們同樣在啟動的時候也要設置Mataspace的值大小。
-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
接著我們在瀏覽器中訪問地址:localhost:8080/nonheap
以上我們就完成了對堆記憶體溢出以及非堆記憶體溢出的演示。
小插曲
在測試非堆記憶體溢出的時候,出現了另外一個錯誤。
java.lang.IncompatibleClassChangeError: Found interface org.objectweb.asm.MethodVisitor, but class was expected
這個異常另外寫在java.lang.IncompatibleClassChangeError,小夥伴如果有遇到,可嘗試一下是否能夠解決
如何查看線上堆記憶體溢出以及非堆記憶體溢出
我們主要查看線上的記憶體映像文件來查看到底是哪裡發生了記憶體溢出。
發生記憶體溢出的主要原因:
1.記憶體發生泄漏
2.記憶體分配不足
假如發生記憶體泄漏的話,我們就需要找到是哪個地方發生了記憶體泄漏,一直佔用記憶體沒有釋放。
下面我們來看一下如何來導出我們的記憶體映像文件。
主要有兩種方式。
1.記憶體溢出自動導出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
第一個參數表示:當發生記憶體溢出的時候,將記憶體溢出文件Dump出來。
第二個參數表示:Dump出來的文件存放的目錄。
2.使用jmap命令手動導出
如果我們使用第一種命令,在發送記憶體溢出的時候再去導出,可能就有點晚了。我們可以等程式運行起來一段時間後,就可以使用jmap命令導出來進行分析。
演示記憶體溢出自動導出
我們需要用到兩個命令參數。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
我們接著運行項目,訪問:localhost:8080/heap
查看一下列印結果。
可以看到,當發生了記憶體溢出後。輸出了一個java_pid3972.hprof的文件。
在當前項目的當前文件中,我們就可以找到該文件。
演示jmap命令
option:-heap,-clstats,-dump:
參數都是什麼意思呢?
live:只導出存活的對象,如果沒有指定,則全部導出
format:導出文件的格式
file:導入的文件
我們剛才的程式還沒有關閉,我們來看下程式的pid是多少。
輸入:jps -l
我們將其文件導入到桌面中來,輸入命令
jmap -dump:format=b,file=heap.hprof 3972
最後的3972是程式的pid。最後可以看到導出完畢。
還有其他的命令參數,小夥伴們可以去官網jmap指令查看如何使用。這裡就不做過多介紹。
下一章節我們將通過命令實戰定位JVM發生死循環、死鎖問題。
推薦
文末
文章收錄至
Github: https://github.com/CoderMerlin/coder-programming
Gitee: https://gitee.com/573059382/coder-programming
歡迎關注並star~