[JVM教程與調優] 瞭解JVM 堆內存溢出以及非堆內存溢出

![[JVM教程與調優] 瞭解JVM 堆內存溢出以及非堆內存溢出.png](https://upload-images.jianshu...html

在上一章中咱們介紹了JVM運行時參數以及jstat指令相關內容:[[JVM教程與調優] 什麼是JVM運行時參數?](https://mp.weixin.qq.com/s?__...。下面咱們來介紹一下jmap+MAT內存溢出。
首先咱們來介紹一下下JVM的內存結構。java

JVM內存結構介紹

JVM內存結構

從圖中咱們能夠看到,JVM的內存結構分爲兩大塊。一塊叫堆區,一塊叫非堆區
堆區又分爲兩大塊,一塊Young,一塊叫OldYoung區又分爲Survivor區和Eden區。Survivor區咱們又分爲S0與S1。能夠結合下圖進行理解git

JVM堆區

非堆區呢,是屬於咱們操做系統的本地內存。它是獨立於咱們堆區以外的。它在JDK1.8裏面有一個新的名字,叫MetaspaceMetaspace裏面還包含幾個塊,其中有一塊就是CCS,還有一塊是CodeCache。固然,在咱們的Metaspace中還包含不少其餘塊,這裏就不作擴展了。github

接下來,咱們來經過實戰,來更加深刻的理解JVM結構,以及出現JVM內存溢出的緣由。web

實戰理解

咱們經過spring.start快速來生成一個springboot項目。spring

快速實戰

如圖,咱們快速的建立一個springboot項目,並將其下載下來。編程

這裏我使用Eclipse,小夥伴們也可使用IDEA或者其餘開發工具也是能夠的。瀏覽器

這裏咱們使用的是SpringBoot工程,若是有的小夥伴對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,這樣就很容易溢出。

訪問測試

啓動時候設置內存參數。
設置內存參數1

設置內存參數2

記得選中咱們的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:<dump-options>,-F

jmap命令

導出內存映像命令

參數都是什麼意思呢?
live:只導出存活的對象,若是沒有指定,則所有導出
format:導出文件的格式
file:導入的文件

咱們剛纔的程序尚未關閉,咱們來看下程序的pid是多少。
輸入:jps -l
查看pid

咱們將其文件導入到桌面中來,輸入命令

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~

微信公衆號

相關文章
相關標籤/搜索