Java OOM問題

閱讀《深入理解Java虛擬機-JVM高級特性與最佳實踐》.周志明 筆記

關於OOM(out of memory),目前有兩方面的接觸。一個是在面試當中面試官會經常問到(面試造航母),另外一個就是在使用IDEA或者eclipse開發項目,項目啓動過程中或者運行時候會出現OutOfMemoryError。線上運行的項目還從來沒有遇到過這樣的問題(但是總會遇到吧)!所以還是有必要去了解甚至是掌握相關的知識的。

對於Java程序員來說,在虛擬機自動內存管理機制的幫助下,不再需要爲每一個new操作去寫配對的delete/free代碼,不容易出現內存泄露和內存溢出問題,由虛擬機管理內存這一切看起來都很美好。不過,也正是因爲Java程序員把內存控制的權力交給了Java虛擬機,一旦出現內存泄露和溢出方面的問題,如果不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會成爲一項異常艱難的工作。

   -----引自 《深入理解Java虛擬機-JVM高級特性與最佳實踐》第2章

根據自己的理解做了如下總結:

有機會出現OOM問題的兩大類情況:

  1. 來自運行時數據區

  2. 直接內存

1.運行時數據區

  大概瞭解一下Java虛擬機運行時數據區的幾部分組成以及可能出現的OOM情況如下

堆(Java Heap):Java 堆是Java虛擬機所管理的內存中最大的一塊,被所有線程共享的內存區域 ,用於存放對象實例,幾乎所有的對象實例都在這裏分配內存。這樣分析來看,如果在堆中沒有內存完成實例分配,並且也無法再擴展時,將會拋出OutOfMemoryError異常。比如:-Xmx:設置JVM最大堆內存大小,以及-Xms:設置JVM最小(啓動時)堆的大小,這兩個值設置的都很小,而且一樣(防止擴展),那麼只要new少許的對象出來,就會出現OutOfMemoryError。

/**
 * @Description 模擬堆內存溢出
 * VM args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args){
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true){
            list.add(new OOMObject());
        }
    }
}

JavaHeapOOM

方法區(Method Area):和Java堆一樣,方法區是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。而運行時常量池屬於方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

/**
 * @Description 運行時常量池導致內存溢出
 * VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
            System.out.println(i);
        }
    }
}

 以上示例,僅僅在JDK1.6會發生OutOfMemoryError:PermGen space。

/**
 * @Description 方法區出現內存溢出
 * VM args: -Xmx10m -Xms10m -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class JavaMethodAreaOOM {
    public static void main(String[] args){
        while(true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.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,objects);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject{
    }
}

以上示例在JDK1.7環境結果爲:

JavaMethodAreaOOM1.7

以上示例在JDK1.8環境結果爲:JavaMethodArea1.8

最後兩行證實:在1.8開始已經沒有永久代這一說法了,取而代之的是另一塊與堆不相連的本地內存—元空間(-XX:MaxMetaspaceSize)。就以上兩個示例作出說明:

在JDK6中,方法區包含的信息,除了JIT編譯生成的代碼放在native memory的CodeCache區域,其他都在永久代;

在JDK7中,Symbol的存儲從PermGen移動到了native memory,並且把靜態變量從instanceklass末尾(位於PermGen內)移動到了java.lang.class對象的末尾(位於Java Heap);

在JDK8中,永久代被徹底移除,取而代之的是另一塊與堆不相連的本地內存—元空間。

Java虛擬機棧和本地方法棧:Java虛擬機棧是線程私有的,生命週期與線程相同,描述着Java方法執行的內存模型:每個方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧。如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠內存就會拋出OutOfMemoryError異常。本地方法棧和虛擬機方法棧作用相似,只是本地方法棧調用的是本地方法(由其他語言實現的JVM底層方法)。

那麼有個問題,當棧空間無法繼續分配時,到底是內存太小,還是已經使用的棧空間太大,其本質上描述的都是一件事。

 通過設置-Xss:棧容量。來複現一下棧溢出的情況:

/**
 * @Description StackOverflowError
 * JVM args: -Xss2M
 */
public class JavaVMStackSOF {
    private int stackLength=-1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF oom= new JavaVMStackSOF();
        try{
            oom.stackLeak();
        }catch (Throwable e){
            System.out.println("Stack length:" +oom.stackLength);
            throw e;
        }
    }
}

StackOverflowOOM

實驗結果表明:在單線程下,無論是由於棧幀太大,還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError。-----引自 《深入理解Java虛擬機-JVM高級特性與最佳實踐》第2章

/**
 * @Description 創建線程導致內存溢出
 * JVM args: -Xss2M
 */
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

JavaStackOOM

通過以上的示例,說明一個問題,高版本的JDK可靠性更高了,如果是從頭開始做一個項目,最好還是使用當前比較新而且穩定的JDK版本。

程序計數器:運行時數據區還有程序計數器的存在,線程私有,一塊很小的內存,Java規範中沒有沒有規定任何OOM情況。

還有一種內存溢出就是直接內存溢出

直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存 區域。但是這部分內存也被頻繁地使用,而且也能導致OOM。在JDK1.4之後,新加入了NIO(new Input/Output),引入了一種基於通道(channel)和緩衝區(Buffer)的I/O方式,它可以使用Native函數直接分配內存,然後通過存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣可以顯著提高性能,避免了Java堆和Native堆中來回複製數據。DirectMemory容量可以通過-XX:MaxDirectMemorySize指定,如果不指定則默認與Java堆最大值(-Xmx指定)一樣。

/**
 * @Description 直接內存溢出
 * JVM args: -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafefield = Unsafe.class.getDeclaredFields()[0];
        unsafefield.setAccessible(true);
        // 返回指定對象上由此Field表示的字段的值。
        // 如果該對象具有原始類型,則該值將自動包裝在對象中。
        Unsafe unsafe = (Unsafe) unsafefield.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

DirectMemoryOOM

關於1.8虛機運行時內存劃分的改動參考,這位博主講的很詳細很明白。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章