JVM內存溢出分析:堆內存溢出+虛擬機+方法區——JVM系列(二)

#JVM 內存溢出

1、堆內存溢出

堆內存中主要存放對象、數組等,只要不斷地創建這些對象,並且保證 GC Roots 到對象之間有可達路徑來避免垃圾收集回收機制清除這些對象,當這些對象所佔空間超過最大堆容量時,就會產生 OutOfMemoryError 的異常。堆內存異常示例如下:

/**

設置最大堆最小堆:-Xms20m -Xmx20m

運行時,不斷在堆中創建OOMObject類的實例對象,且while執行結束之前,GC Roots(代碼中的oomObjectList)到對象(每一個OOMObject對象)之間有可達路徑,垃圾收集器就無法回收它們,最終導致內存溢出。

*/

public class HeapOOM {

static class OOMObject {

}

public static void main(String[] args) {

    List<OOMObject> oomObjectList = new ArrayList<>();

    while (true) {

        oomObjectList.add(new OOMObject());

    }

}
}

運行後會報異常,在堆棧信息中可以看到:

java.lang.OutOfMemoryError: Java heap space 的信息,說明在堆內存空間產生內存溢出的異常。

新產生的對象最初分配在新生代,新生代滿後會進行一次 Minor GC,如果 Minor GC 後空間不足會把該對象和新生代滿足條件的對象放入老年代,老年代空間不足時會進行 Full GC,之後如果空間還不足以存放新對象則拋出 OutOfMemoryError 異常。

常見原因:內存中加載的數據過多如一次從數據庫中取出過多數據;集合對對象引用過多且使用完後沒有清空;代碼中存在死循環或循環產生過多重複對象;堆內存分配不合理;網絡連接問題、數據庫問題等。

2、虛擬機棧/本地方法棧溢出

(1)StackOverflowError:當線程請求的棧的深度大於虛擬機所允許的最大深度,則拋出StackOverflowError,簡單理解就是虛擬機棧中的棧幀數量過多(一個線程嵌套調用的方法數量過多)時,就會拋出StackOverflowError異常。

最常見的場景就是方法無限遞歸調用,如下:

/**

設置每個線程的棧大小:-Xss256k

運行時,不斷調用doSomething()方法,main線程不斷創建棧幀併入棧,導致棧的深度越來越大,最終導致棧溢出。

*/

public class StackSOF {

private int stackLength=1;

public void doSomething(){

        stackLength++;

        doSomething();

}

public static void main(String[] args) {

    StackSOF stackSOF=new StackSOF();

    try {

        stackSOF.doSomething();

    }catch (Throwable e){//注意捕獲的是Throwable

        System.out.println("棧深度:"+stackSOF.stackLength);

        throw e;

    }

}
}

上述代碼執行後拋出:

Exception in thread “Thread-0” java.lang.StackOverflowError 的異常。

(2)OutOfMemoryError:如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出 OutOfMemoryError。

我們可以這樣理解,虛擬機中可以供棧佔用的空間≈可用物理內存 - 最大堆內存 - 最大方法區內存,比如一臺機器內存爲 4G,系統和其他應用佔用 2G,虛擬機可用的物理內存爲 2G,最大堆內存爲 1G,最大方法區內存爲 512M,那可供棧佔有的內存大約就是 512M,假如我們設置每個線程棧的大小爲 1M,那虛擬機中最多可以創建 512個線程,超過 512個線程再創建就沒有空間可以給棧了,就報 OutOfMemoryError 異常了。
image

棧上能夠產生 OutOfMemoryError 的示例如下:

/**

設置每個線程的棧大小:-Xss2m

運行時,不斷創建新的線程(且每個線程持續執行),每個線程對一個一個棧,最終沒有多餘的空間來爲新的線程分配,導致OutOfMemoryError

*/

public class StackOOM {

private static int threadNum = 0;

public void doSomething() {

    try {

        Thread.sleep(100000000);

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

}

public static void main(String[] args) {

    final StackOOM stackOOM = new StackOOM();

    try {

        while (true) {

            threadNum++;

            Thread thread = new Thread(new Runnable() {

                @Override

                public void run() {

                    stackOOM.doSomething();

                }

            });

            thread.start();

        }

    } catch (Throwable e) {

        System.out.println("目前活動線程數量:" + threadNum);

        throw e;

    }

}
}

上述代碼運行後會報異常

在堆棧信息中可以看到java.lang.OutOfMemoryError: unable to create new native thread的信息,無法創建新的線程,說明是在擴展棧的時候產生的內存溢出異常。

總結:在線程較少的時候,某個線程請求深度過大,會報 StackOverflow 異常,解決這種問題可以適當加大棧的深度(增加棧空間大小),也就是把 -Xss 的值設置大一些,但一般情況下是代碼問題的可能性較大;在虛擬機產生線程時,無法爲該線程申請棧空間了。

會報 OutOfMemoryError 異常,解決這種問題可以適當減小棧的深度,也就是把 -Xss 的值設置小一些,每個線程佔用的空間小了,總空間一定就能容納更多的線程,但是操作系統對一個進程的線程數有限制,經驗值在 3000~5000 左右。

在 jdk1.5 之前 -Xss 默認是 256k,jdk1.5 之後默認是 1M,這個選項對系統硬性還是蠻大的,設置時要根據實際情況,謹慎操作。

3、方法區溢出

前面說到,方法區主要用於存儲虛擬機加載的類信息、常量、靜態變量,以及編譯器編譯後的代碼等數據,所以方法區溢出的原因就是沒有足夠的內存來存放這些數據。

由於在 jdk1.6 之前字符串常量池是存在於方法區中的,所以基於 jdk1.6 之前的虛擬機,可以通過不斷產生不一致的字符串(同時要保證和 GC Roots 之間保證有可達路徑)來模擬方法區的 OutOfMemoryError 異常;但方法區還存儲加載的類信息,所以基於 jdk1.7 的虛擬機,可以通過動態不斷創建大量的類來模擬方法區溢出。

/**

設置方法區最大、最小空間:-XX:PermSize=10m -XX:MaxPermSize=10m

運行時,通過cglib不斷創建JavaMethodAreaOOM的子類,方法區中類信息越來越多,最終沒有可以爲新的類分配的內存導致內存溢出

*/

public class JavaMethodAreaOOM {

public static void main(final String[] args){

   try {

       while (true){

           Enhancer enhancer=new Enhancer();

           enhancer.setSuperclass(JavaMethodAreaOOM.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();

       }

   }catch (Throwable t){

       t.printStackTrace();

   }

}
}

上述代碼運行後會報:

java.lang.OutOfMemoryError: PermGen space 的異常,說明是在方法區出現了內存溢出的錯誤。

4、本機直接內存溢出

本機直接內存(DirectMemory)並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域,但 Java 中用到 NIO 相關操作時(比如 ByteBuffer 的 allocteDirect 方法申請的是本機直接內存),也可能會出現內存溢出的異常。

總結

JVM內存區域劃分,便於它能夠更加高效的管理自身的內存。當程序中出現這種由於JVM造成的內存溢出的情況的時候,需要根據不同的情況做不同的分析與處理。

讀者分享

針對於上面的文章我總結出了互聯網公司java程序員面試涉及到的絕大部分面試題及答案做成了文檔和架構視頻資料免費分享給大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術資料),希望能幫助到您面試前的複習且找到一個好的工作,也節省大家。

資料免費領取方式:點擊免費獲取!

更多架構專題及視頻資料展示如下:

資料免費領取方式:328993819 找管理小姐姐免費獲取!

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