引言:本文是閱讀經典的《深入理解Java虛擬機》後,對第二章的內存溢出異常情況進行總結,通過實際代碼實現來驗證知識點的正確性。
1.堆內存溢出
Java的堆用於存儲對象實例,只要不斷地創建對象,並且保證GC root 到對象之間有可達路徑,就無法被收集器回收。
在java工程的目錄下新建一個類,這裏命名爲MemoryController,代碼如下:
@RestController
public class MemoryController {
private List<TestEntity> heapList = new ArrayList<>();
/**
* -Xmx32M -Xms32M
* */
@GetMapping("/heap")
public String heap() {
long i=1l;
while(true) {
heapList.add(new TestEntity(i++, UUID.randomUUID().toString()));
}
}
}
運行大約十分鐘之後,結果顯示是堆內存溢出:
《深入理解Java虛擬機》書中給出這種情況的解決思路:
(1)如果是內存泄露,查GCRoot引用鏈,打印出引用鏈信息,就基本可以定位泄露代碼的爲位置。
(2)如果不存在泄露,也就是內存中的對象確實都必須存活,就檢查虛擬機的堆參數。看看-Xmx和-Xms在機器物理內存的基礎上可否再調大。
具體方法可見另一篇博客:Java虛擬機(JVM)調優和Debug的常用參數詳解
不過大多數情況都是在代碼層面上檢查是否有對象生命週期過長或關聯過多,嘗試減少程序運行期的內存消耗即可。
2. 元數據區溢出
《深入理解Java虛擬機》中稱作方法區,JDK1.7進行了“去永久代”之後,就成了元數據區,用於存Class對象,一個類要被垃圾收集器回收的條件很苛刻,尤其是在框架越來越多的情況下。因此這裏創建大量不回收的類對象。
首先,在工程的pom.xml中引入asm的jar包幫助實現動態生成類:
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
創建動態生成類Metaspace,代碼如下:
public class Metaspace extends ClassLoader {
public static Collection<? extends 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;
}
}
在之前的MemoryController里加metaspace()方法
@RestController
public class MemoryController {
List<Class<?>> classlist = new ArrayList<>();
/**
* -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
*/
@GetMapping("/metaspace")
public String metaspace() {
long i=1l;
while(true) { //動態生成class
classlist.addAll(Metaspace.createClasses());
}
}
}
爲了能夠馬上看到結果,這裏手動把JVM的元數據去參數修改一下,IDEA裏的修改如下圖
很快直接元數據區溢出,並且spring直接定位到了導致溢出的代碼行
本文主要測試了兩種典型的內存溢出,還有棧溢出(遞歸調用可能溢出)以及本機溢出以可測試,但是出現情況較少,就沒有做實際的測試了。