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


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

在上一章中我們介紹了JVM運行時參數以及jstat指令相關內容:[JVM教程與調優] 什么是JVM運行時參數?。下面我們來介紹一下jmap+MAT內存溢出。
首先我們來介紹一下下JVM的內存結構。

JVM內存結構介紹

JVM內存結構

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

JVM堆區

非堆區呢,是屬於我們操作系統的本地內存。它是獨立於我們堆區之外的。它在JDK1.8里面有一個新的名字,叫MetaspaceMetaspace里面還包含幾個塊,其中有一塊就是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,這樣就很容易溢出。

訪問測試

啟動時候設置內存參數。
設置內存參數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: ,-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~

微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM