堆溢出、棧溢出、永久代溢出、直接內存溢出

                堆溢出、棧溢出、永久代溢出、直接內存溢出

  1. 棧溢出(StackOverflowError)
  2. 堆溢出(OutOfMemoryError:Java heap space)
  3. 永久代溢出(OutOfMemoryError: PermGen space)
  4. 直接內存溢出

一、堆溢出

創建對象時如果沒有可以分配的堆內存,JVM就會拋出OutOfMemoryError:java heap space異常。

堆溢出實例:

/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    int i=0;
    while(true){
        list.add(new byte[5*1024*1024]);
        System.out.println("分配次數:"+(++i));
    }
}

運行結果:
分配次數:1
分配次數:2
分配次數:3

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2464.hprof ...
Heap dump file created [16991068 bytes in 0.047 secs]

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at com.ghs.test.OOMTest.main(OOMTest.java:16)

附:dump文件會在項目的根目錄下生成

從上面的例子我們可以看出,在進行第4次內存分配時,發生了內存溢出。

二、棧溢出


棧空間不足時,需要分下面兩種情況處理:

線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError
虛擬機在擴展棧深度時無法申請到足夠的內存空間,將拋出OutOfMemberError
附:當前大部分的虛擬機棧都是可動態擴展的。

1、棧空間不足——StackOverflowError實例

public class StackSOFTest {

    int depth = 0;

    public void sofMethod(){
        depth ++ ;
        sofMethod();
    }

    public static void main(String[] args) {
        StackSOFTest test = null;
        try {
            test = new StackSOFTest();
            test.sofMethod();
        } finally {
            System.out.println("遞歸次數:"+test.depth);
        }
    }
}

執行結果:
遞歸次數:982
Exception in thread "main" java.lang.StackOverflowError
    at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:8)
    at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)
    at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)
……後續堆棧信息省略

2、棧空間不足——OutOfMemberError實例
單線程情況下,不論是棧幀太大還是虛擬機棧容量太小,都會拋出StackOverflowError,導致單線程情境下模擬棧內存溢出不是很容易,不過通過不斷的建立線程倒是可以產生內存溢出異常。

public class StackOOMTest {

    public static void main(String[] args) {
        StackOOMTest test = new StackOOMTest();
        test.oomMethod();
    }

    public void oomMethod(){
        while(true){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    loopMethod();
                }
            }).start();;
        }
    }

    private void loopMethod(){
        while(true){

        }
    }
}

運行結果:
……操作系統直接掛掉了

三、永久代溢出

永久代溢出可以分爲兩種情況,第一種是常量池溢出,第二種是方法區溢出。

1、永久代溢出——常量池溢出
要模擬常量池溢出,可以使用String對象的intern()方法。如果常量池包含一個此String對象的字符串,就返回代表這個字符串的String對象,否則將String對象包含的字符串添加到常量池中。

public class ConstantPoolOOMTest {

    /**
     * VM Args:-XX:PermSize=10m -XX:MaxPermSize=10m
     * @param args
     */
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i=1;
        try {
            while(true){
                list.add(UUID.randomUUID().toString().intern());
                i++;
            }
        } finally {
            System.out.println("運行次數:"+i);
        }
    }
}

運行結果:
……比較尷尬的是,通過intern,始終無法模擬出常量池溢出,我的猜想是JDK7對常量池做了優化。
如果哪位大神成功模擬出來了,還望指點一二。

找了好久,終於弄清楚了使用string.intern()方法無法模擬常量池溢出的原因。

因爲在JDK1.7中,當常量池中沒有該字符串時,JDK7的intern()方法的實現不再是在常量池中創建與此String內容相同的字符串,而改爲在常量池中記錄Java Heap中首次出現的該字符串的引用,並返回該引用。
簡單來說,就是對象實際存儲在堆上面,所以,讓上面的代碼一直執行下去,最終會產生堆內存溢出。
下面我將堆內存設置爲:-Xms5m -Xmx5m,執行上面的代碼,運行結果如下:

運行次數:58162
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.lang.Long.toUnsignedString(Unknown Source)
    at java.lang.Long.toHexString(Unknown Source)
    at java.util.UUID.digits(Unknown Source)

    at java.util.UUID.toString(Unknown Source)
    at com.ghs.test.ConstantPoolOOMTest.main(ConstantPoolOOMTest.java:18)

2、永久代溢出——方法區溢出
方法區存放Class的相關信息,下面藉助CGLib直接操作字節碼,生成大量的動態類。

public class MethodAreaOOMTest {

    public static void main(String[] args) {
        int i=0;
        try {
            while(true){
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        return proxy.invokeSuper(obj, args);
                    }
                });
                enhancer.create();
                i++;
            }
        } finally{
            System.out.println("運行次數:"+i);
        }
    }

    static class OOMObject{

    }
}

運行結果:

運行次數:56
Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

四、直接內存溢出

DirectMemory可以通過-XX:MaxDirectMemorySize指定,如果不指定,默認與Java堆的最大值(-Xmx指定)一樣。
NIO會使用到直接內存,你可以通過NIO來模擬,在下面的例子中,跳過NIO,直接使用UnSafe來分配直接內存。

public class DirectMemoryOOMTest {

    /**
     * VM Args:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m
     * @param args
     */
    public static void main(String[] args) {
        int i=0;
        try {
            Field field = Unsafe.class.getDeclaredFields()[0];
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            while(true){
                unsafe.allocateMemory(1024*1024);
                i++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("分配次數:"+i);
        }
    }
}

運行結果:
Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at com.ghs.test.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:20)
分配次數:27953

總結:


棧內存溢出:程序所要求的棧深度過大。
堆內存溢出: 分清內存泄露還是 內存容量不足。泄露則看對象如何被 GC Root 引用,不足則通過調大-Xms,-Xmx參數。
永久代溢出:Class對象未被釋放,Class對象佔用信息過多,有過多的Class對象。
直接內存溢出:系統哪些地方會使用直接內存。
 

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