JVM04-JVM中內存溢出以及其處理方法

前言

上一篇我們介紹了JVM03–JVM垃圾收集機制的一些基本概念,這一篇介紹一下JVM中各種內存溢出及其處理方法。
本文會按照JVM中內存劃分來介紹各種內存溢出的例子。

一些基本的設置說明

爲了模擬出內存溢出的效果,我們需要手動設置內存區域的內存大小,下面就是設置值部分設置值及其說明。

分類 選項 說明
虛擬機棧 -Xss 每個線程的棧大小
堆空間 -Xms 啓動JVM時的初始堆大小
堆空間 -Xmx 堆空間最大值
堆空間 -Xmn 新生代大小(1.4or lator) 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小.增大年輕代後,將會減小年老代大小.此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8
新生代空間 -XX:NewRatio 新生代與老年代的比例
新生代空間 -XX:NewSize 新生代大小
新生代空間 -XX:SurvivorRation Eden區域SurvivorRation區的比例
永久代空間 -XX:PermSize 啓動JVM時的初始永久代大小
永久代空間 -XX:MaxPermSize 永久代空間最大值
元空間 -XX:MetaspaceSize 指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整;如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過 -XX:MaxMetaspaceSize 的情況下,適當提高該值。
元空間 -XX:MaxMetaspaceSize 設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小
元空間 -XX:MaxMetaspaceFreeRatio 在垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因爲元空間不足導致的垃圾收集的頻率

堆內存溢出

Java堆用於存儲對象的實例,我們只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼隨着對象數量的增加,總容量觸及最大堆的容量限制後就會產生內存溢出。Java堆內存的OutOfMemoryError異常是實際應用中最常見的內存溢出異常情況。出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟隨進一步提示“Java heap space”。下面舉個例子來模擬堆內存溢出。這裏將-Xms-Xmx都設置成20M,保證了Java堆內存不可擴展。然後,通過-XX:HeapDumpPath指定dump文件的保存位置。這裏通過while循環不斷的創建對象,然後保存到集合中。

/**
 * Java堆內存異常
 VM Args:
 //這兩個參數保證了堆中的可分配內存固定爲20M
 -Xms20m
 -Xmx20m

 -XX:+HeapDumpOnOutOfMemoryError //自動生成dump文件
 //文件生成的位置,作爲生成在桌面的一個目錄
 -XX:HeapDumpPath=D:\srv\dump
 *
 * @author xiang.wei
 * @date 2020/5/27 13:35
 */
public class HeapOOM {
    /**
     * 創建一個內部類用於創建對象使用
     */
    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        //無限的創建對象放在堆中
        while (true) {
            list.add(new OOMObject());
        }
    }
}

下面簡單的說一下在Idea中設置應用運行內存的方法,我們只需要在 Run---->Edit Configurations—>找到需要設置的主類,然後在VM options中添加

 -Xms20M -Xmx20M -Xmn20M   -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\srv\dump

如下圖所示:
在這裏插入圖片描述
上面程序運行結果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\srv\dump\java_pid23624.hprof ...
Heap dump file created [28062091 bytes in 0.143 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.jay.service.HeapOOM.main(HeapOOM.java:31)
Java HotSpot(TM) 64-Bit Server VM warning: MaxNewSize (20480k) is equal to or greater than the entire heap (20480k).  A new max generation size of 19968k will be used.

我們可以看到程序生成了dump文件到指定目錄,打開jvisualvm.exe工具導入heapDump文件,相應的說明如下圖所示:
在這裏插入圖片描述
一共創建了802858個實例,消耗了 98.9%的運行內存。
切換到實例數如下圖所示:
在這裏插入圖片描述
如何解決堆內存的OOM異常呢,首先需要確認內存中導致OOM的對象是否是必要的,也就是要先分清楚到底是出現了內存泄露(Memory Leak)還是內存溢出(Memory Overflow)。

Java棧內存異常

說完了Java堆內存異常,下面我們來看看Java棧內存異常,在實際開發中發生棧內存異常的情況比較少。Java棧內存異常發生的兩種情況是:

  1. 如果線程請求的棧深度(棧深度:指目前虛擬機棧中沒有出棧的方法幀)大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。這種情況在遞歸的場景下可能會出現,如果在使用遞歸算法時沒有控制好遞歸的跳出條件,就有可能會出現這種情況,下面的例子就是一個沒有跳出條件的遞歸調用。根據前面說明,我們可以通過-Xss160k設置棧容量爲160K。
  2. 如果虛擬機的棧內存允許動態擴展,當擴展容量無法申請到足夠的內存時,將拋出OutOfMemoryError異常。
    如下這個例子:
/**
 * VM Args:
 *    設置棧容量爲160K,默認1M
 *    -Xss160k
 * @author xiang.wei
 * @date 2020/5/27 16:09
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        System.out.println("*********棧的深度是:" + stackLength);
        //遞歸調用
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Exception e) {
            throw e;
        }
    }
}

運行結果如下:

*********棧的深度是:1287
Exception in thread "main" java.lang.StackOverflowError
	at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)

可以看到,遞歸調用了1287次,棧容量不夠用了。
默認的棧容量在正常的方法調用時,棧深度可以達到1000-2000深度,所以,一般的遞歸可以承受的住,如果代碼中出現了StackOverflowError,首先需要檢查代碼,看看是不是遞歸寫的不對。不能只是調參數。
當創建很多線程時,容易出現OOM(OutOfMemoryError),這時可以通過具體情況,減少最大堆容量,或者棧容量來解決問題。
線程數(最大棧容量)+最大堆值+其他內存(忽略不計或者一般不改動)=機器最大內存*
當線程數比較多時,且無法通過業務上減少線程數,再不換機器的情況下,我們只能把最大棧容量設置小一點,或者把最大堆值設置小一點。

方法區和運行時常量池溢出

由於運行時常量池是方法區的一部分,所以這兩個區域的溢出測試可以放在一起進行。需要注意的是HotSpot從JDK7開始逐步“去永久代”的計劃,並在JDK8中完全使用元空間代替永久代,使用"永久代"還是"元空間"來實現方法區,對程序的影響是不同的。
元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存,因此,默認情況下元空間的大小僅受本地內存限制,但仍可以通過參數控制:
-XX:MetaspaceSize與-XX:MaxMetaspaceSize來控制大小。
方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收,要達成的條件是比較苛刻的。在經常運行時生成大量動態類的應用場景裏,就應該特別關注這些類的回收狀況,這類場景除了之前提到的程序使用了CGLib字節碼增強和動態語言外,常見的還有:大量JSP或動態產生JSP文件的應用(JSP第一次運行時需要編譯爲Java類)、基於OSGI的應用(即使是同一個類文件,被不同的加載器加載也會被視爲不同的類)。
方法區的主要職責是用於存放類型的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。 對於這部分區域的測試,基本的思路是運行時產生大量的類去填滿方法區,直到溢出爲止。
這是因爲在調用CGLib創建代理時會生成動態代理類,即Class對象到Metaspace,所以while一下就出異常了。
下面這個例子就是通過設置元空間大小,然後,通過動態代理生成大量的類,來模擬元空間內存溢出的情況。

/**
 * 在JDK8下測試方法區,所以設置了Metaspace的大小爲固定的8M
 -XX:MetaspaceSize=8m
 -XX:MaxMetaspaceSize=8m
 * @author xiang.wei
 * @date 2020/5/27 17:29
 */
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(JOOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, objects, methodProxy) -> methodProxy.invokeSuper(obj,objects));
            //無限創建動態代理,生成class對象
            enhancer.create();
        }
    }
    static class JOOMObject{}

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:538)
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)

總結

本文首先介紹了堆內存溢出(OutOfMemoryError)發生的場景以及處理方式,OutOfMemoryError發生的場景主要就是系統創建了大量的對象,並且這些對象是有效的(即保證GC Roots到對象之間有可達路徑)。然後,介紹了棧內存異常(StackOverflowError)的發生場景以及處理方式,StackOverflowError發生的場景主要是線程調用棧深度超過了虛擬機運行的棧深度。最後,介紹了方法區的內存溢出。需要注意的是JDK1.8中完全移除了永久代,取而代之的是元空間。

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