論JVM爆炸的幾種姿勢及自救方法

深夜裡的程式猿發表於2019-04-08

前言

如今不管是在面試還是在我們的工作中,OOM總是不斷的出現在我們的視野中,所以我們有必要去了解一下導致OOM的原因以及一些基本的調整方法,大家可以通過下面的事例來了解一下什麼樣的程式碼會導致OOM,幫助我們以後在工作中能夠通過異常資訊來判斷是JVM裡面哪個區域出現了問題。

先介紹一下筆者的相關編碼環境。

jdk:java version "1.8.0_121"

ide:IntelliJ IDEA 2019.1 (Community Edition)


正文

1.Java堆溢位

Java中的堆儲存的都是物件例項,當我們不斷的建立物件,而GC的時候又不能回收,當儲存的物件大小超過了-Xmx的值,這時候則會出現OutOfMemoryError.[-XX:+HeapDumpOnOutOfMemoryError]引數可以讓jvm出現記憶體溢位的時候dump出記憶體堆轉儲快照。

/**
 * VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * @author wangzenghuang
 */
public class HeapOOMDemo {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        while(true){
            stringList.add("str");
        }
    }
}

複製程式碼

執行結果,發生OOM,並且在我們專案的根目錄dump出當前的記憶體堆快照

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1376.hprof ...
Heap dump file created [7972183 bytes in 0.047 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at HeapOOMDemo.main(HeapOOMDemo.java:12)

Process finished with exit code 1
複製程式碼

簡單解決思路

那麼發生這個問題以後我們的解決思路有哪些呢?我們可以利用一些工具(例如Eclipse Memory Analyzer )來分析dump出的檔案,一般來說,當生產環境發生OOM,比較常見的一個原因是發生了記憶體洩漏,用工具可以分析出洩露的物件到GC Root的引用鏈,從而定位到問題程式碼。假如經過分析後發現記憶體中的物件都是“必須存活”的物件,這時候就要思考下專案中是否把“-Xms跟-Xmx”設定得太小了(當然這裡也不是隨意調大,需要結合機器的實體記憶體情況),再者需要留意程式碼中是否有一些長生命週期的物件,從程式碼中優化記憶體消耗。

2.方法區溢位

在jvm的方法區中,它主要存放了類的資訊,常量,靜態變數等。在jdk8以前是通過“-XX:PermSize,-XX:MaxPermSize”來調整這個區域的值,但是從8開始呢,永久代的概念被MetaSpace(元空間)代替了,對應的引數也變成了“-XX:MetaspaceSize,-XX:MaxMetaspaceSize”。在這個例子中使用CGLib來動態生成一些類,方便我們實驗操作。

/**
 * VM Args: -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=5m
 * @author wangzenghuang
 */
public class MethodAreaOOMDemo {
    public static void main(String[] args) {
        while(true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(obj,objects);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject{}
}
複製程式碼

執行結果

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
複製程式碼

簡單解決方法

這個問題的話,一般來說根據情況調整方法區的大小就行了,網上也有人說可以去掉MetaSpace的的大小限制,但是不建議這麼幹,畢竟不可控的事情我們要少點幹,很容易給自己埋雷。

3.棧溢位

對於我們來說,還有一個熟悉的錯誤,那就是“StackOverflowError”,它是由執行緒請求的棧深度超過了jvm允許的最大範圍而產生的。“-Xss”引數可以設定棧容量。

/**
 * VM Args: -Xss128k
 * @author wangzenghuang
 */
public class StackOFDemo {
    private static int stackLength = 1;

    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        StackOFDemo stackOFDemo = new StackOFDemo();
        try {
            stackOFDemo.stackLeak();
        }catch (Throwable e){
            System.out.println("length : "+ stackLength);
            throw e;
        }
    }
}
複製程式碼

執行結果

length : 983
Exception in thread "main" java.lang.StackOverflowError
	at StackOFDemo.stackLeak(StackOFDemo.java:10)
	at StackOFDemo.stackLeak(StackOFDemo.java:10)
    ...
複製程式碼

簡單解決思路

一般來說此類問題多出現在存在遞迴的地方,要從程式碼裡重新審視遞迴未結束的原因,若遞迴的方法沒問題可以根據實際情況調整“-Xss”引數的大小。還有一些程式碼的循壞依賴也會造成此類情況,

4.直接記憶體溢位

本機直接記憶體預設與“-Xmx”設定的值一樣大,可以通過“-XX:MaxDirectMemorySize”修改。

/**
 * VM Args: -Xmx20m -XX:MaxDirectMemorySize=10
 * @author wangzenghuang
 */
public class DirectMemoryOOMDemo {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while (true){
            unsafe.allocateMemory(_1MB);
        }
    }
}

複製程式碼

執行結果

呃,一執行這段程式碼idea直接閃退了,查閱其他資料可以得知當DirectMemory導致記憶體溢位時,Heap Dump檔案是很小的,如果程式中有使用NIO的情況可以檢查一下。

總結

這裡所展示的程式碼只是可以觸發jvm的各種錯誤,但是並不代表這是唯一的觸發錯誤的方方式,假如我們的程式碼比較複雜,有時候遇到類似錯誤的時候還是需要耐心分析。

論JVM爆炸的幾種姿勢及自救方法

喜歡的話,關注一下公眾號《深夜裡的程式猿》吧,每天分享最乾的乾貨

相關文章