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);
}
}
}
執行結果