OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。 要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪 ...
OOM是什麼?英文全稱為 OutOfMemoryError(記憶體溢出錯誤)。當程式發生OOM時,如何去定位導致異常的代碼還是挺麻煩的。
要檢查OOM發生的原因,首先需要瞭解各種OOM情況下會報的異常信息。這樣能縮小排查範圍,再結合異常堆棧、heapDump文件、JVM分析工具和業務代碼來判斷具體是哪些代碼導致的OOM。筆者在此測試並記錄以下幾種OOM情況。
環境準備
- jdk1.8(HotSpot虛擬機)
- windows操作系統
- idea開發工具
在idea上進行測試時,需要瞭解idea執行測試用例如何設置虛擬機參數(VM options)。如下圖所示:
-
單擊main方法的啟動圖標,選擇修改運行配置
-
打開Add VM options,將JVM參數填在圖示VM options處
堆溢出
Java堆是用來存儲對象實例的,只要不斷的創建對象,並保證對象不被GC回收掉,那麼當對象占用的記憶體達到了最大堆記憶體限制,無法再申請到新的記憶體空間時,就會導致OOM。要讓對象不被回收就需要保證GC Roots引用鏈可以到達該對象,此處採用了List來保持對對象的引用。並且設置參數-XX:+HeapDumpOnOutOfMemoryError列印OOM發生時的堆記憶體狀態。代碼如下:
/**
* VM options: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* @author yywf
* @date 2024/4/11
*/
public class HeapOOMTest {
public static void main(String[] args) {
List<Object> list = new LinkedList<>();
while (true) {
list.add(new Object());
}
}
}
執行結果
提示信息為GC overhead limit exceeded。
使用JProfiler打開heapDump文件,可以看到啟動類載入器中的java.util.LinkedList占用了92.3%的堆記憶體
字元串常量池溢出
通過String.intern()這個native方法將字元串添加到常量池中。
測試代碼如下:
/**
* VM options: -Xms2M -Xmx2M
* @author yywf
* @date 2024/4/11
*/
public class StringConstantOOMTest {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
執行結果
在jdk8中,字元串常量池已經移到了堆中。所以拋出的異常是堆記憶體溢出。
棧溢出
在JVM規範中,棧有虛擬機棧和本地方法棧之分。但在實際的實現中,HotSpot虛擬機是沒有區分虛擬機棧和本地方法棧的。所以對於HotSpot來說,-Xoss(設置本地方法棧大小)參數是無效的,棧容量只能通過-Xss參數設置。
棧深度造成的溢出
在JVM規範中,如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。測試代碼如下:
/**
* VM options: -Xss128k
* @author yywf
* @date 2024/4/11
*/
public class StackOOMTest {
private int stackLength = 1;
public void stackDeep() {
stackLength++;
stackDeep();
}
public static void main(String[] args) {
StackOOMTest test = new StackOOMTest();
try {
test.stackDeep();
} catch (Throwable e) {
System.out.println("棧深度:" + test.stackLength);
throw e;
}
}
}
執行結果
創建線程造成的記憶體溢出
另一種情況,機器的RAM記憶體是固定的,如果不考慮其他程式占用記憶體,那麼RAM就由堆、方法區、程式計數器、虛擬機棧和本地方法棧瓜分。通過不斷的創建線程占滿RAM的記憶體,會導致什麼情況呢?測試代碼:
/**
* VM options: -Xss10M
* @author yywf
* @date 2024/4/11
*/
public class CreateThreadOOMTest {
public void stackOOMByThread() {
while (true) {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
public static void main(String[] args) {
CreateThreadOOMTest test = new CreateThreadOOMTest();
test.stackOOMByThread();
}
}
這裡把棧的大小設置為了10M,也就是說創建一個線程最少需要10M的記憶體。可以更快的出現結果。
執行結果
拋出的是OutOfMemoryError。慎用慎用慎用,重要的事情說三遍,本人在測試的時候電腦死機了一會。得虧線上程的run方法中讓線程睡眠了,不然cpu+記憶體雙雙陣亡。
方法區溢出
方法區大小在jdk1.7(包含)以前版本是通過-XX:PermSize和-XX:MaxPermSize來設置的。在jdk8的實現叫做元空間(metaspace),通過-XX:MetaspaceSize=10M和-XX:MaxMetaspaceSize=10M來設置其大小。
方法區存放的是類的信息,所以在運行時不斷創建類就行。這裡使用CGLib動態代理來生成類,可以添加以下maven依賴來使用CGLib:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.13</version>
</dependency>
測試代碼如下:
/**
* VM options: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
* @author yywf
* @date 2024/4/11
*/
public class MetaSpaceOomTest {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
}
}
執行結果
本機直接記憶體溢出
通過-XX:MaxDirectMemorySize=10M參數設置能申請的DirectMemory大小。如果不設置則預設為java堆的最大值。通過反射獲取Unsafe實例,使用其來申請DirectMemory記憶體。
測試代碼如下:
/**
* VM options: -Xmx10M -XX:MaxDirectMemorySize=10M
* @author yywf
* @date 2024/4/11
*/
public class DirectOOMTest {
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(1048576);
}
}
}
執行結果