JVM(三)OutOfMemoryError異常

1 Java堆溢出

堆內(nèi)存中主要存放對(duì)象、數(shù)組等,只要不斷地創(chuàng)建這些對(duì)象,并且保證 GC Roots 到對(duì)象之間有可達(dá)路徑來避免垃
圾收集回收機(jī)制清除這些對(duì)象,當(dāng)這些對(duì)象所占空間超過最大堆容量時(shí),就會(huì)產(chǎn)生 OutOfMemoryError 的異常。堆
內(nèi)存異常示例如下:

public class HeapOOM {
    // 設(shè)置JVM參數(shù)最大堆和最小堆:-Xms20m -Xmx20m
    static class OOMObject {}

    public static void main(String[] args) {
        List<OOMObject> oomObjectList = new ArrayList<>();
        while (true) {
            oomObjectList.add(new OOMObject());
        }
    }
}

運(yùn)行后會(huì)報(bào)異常,在堆棧信息中可以看到:
java.lang.OutOfMemoryError: Java heap space,說明在堆內(nèi)存空間產(chǎn)生內(nèi)存溢出的異常。
新產(chǎn)生的對(duì)象最初分配在新生代,新生代滿后會(huì)進(jìn)行一次 Minor GC ,如果 Minor GC 后空間不足會(huì)把該對(duì)象和新生代滿足條件的對(duì)象放入老年代,老年代空間不足時(shí)會(huì)進(jìn)行 Full GC ,之后如果空間還不足以存放新對(duì)象則拋出 OutOfMemoryError 異常。
常見原因:

  • 內(nèi)存加載數(shù)據(jù)過多,如一次從數(shù)據(jù)庫(kù)中取出過多數(shù)據(jù)
  • 集合對(duì)對(duì)象引用過多且使用完后沒有清空。
  • 代碼中存在死循環(huán)或者循環(huán)產(chǎn)生過多的重復(fù)對(duì)象
  • 堆內(nèi)存分配不合理

2 虛擬機(jī)棧和本地方法棧溢出

由于HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧, 因此對(duì)于HotSpot來說, -Xoss參數(shù)(設(shè)置本地方法棧大?。?雖然存在, 但實(shí)際上是沒有任何效果的, 棧容量只能由-Xss參數(shù)來設(shè)定。 關(guān)于虛擬機(jī)棧和本地方法棧, 在 《Java虛擬機(jī)規(guī)范》 中描述了兩種異常:
1) 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度, 將拋出StackOverflowError異常。
2) 如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展, 當(dāng)擴(kuò)展棧容量無法申請(qǐng)到足夠的內(nèi)存時(shí), 將拋出 OutOfMemoryError異常。
《Java虛擬機(jī)規(guī)范》 明確允許Java虛擬機(jī)實(shí)現(xiàn)自行選擇是否支持棧的動(dòng)態(tài)擴(kuò)展, 而HotSpot虛擬機(jī)的選擇是不支持?jǐn)U展, 所以除非在創(chuàng)建線程申請(qǐng)內(nèi)存時(shí)就因無法獲得足夠內(nèi)存而出現(xiàn) OutOfMemoryError異常, 否則在線程運(yùn)行時(shí)是不會(huì)因?yàn)閿U(kuò)展而導(dǎo)致內(nèi)存溢出的, 只會(huì)因?yàn)闂H萘繜o法容納新的棧幀而導(dǎo)致StackOverflowError異常。 為了驗(yàn)證這點(diǎn), 我們可以做兩個(gè)實(shí)驗(yàn), 先將實(shí)驗(yàn)范圍限制在單線程中操作, 嘗試下面兩種行為是 否能讓HotSpot虛擬機(jī)產(chǎn)生OutOfMemoryError異常: 使用-Xss參數(shù)減少棧內(nèi)存容量。 結(jié)果: 拋出StackOverflowError異常, 異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。
定義了大量的本地變量, 增大此方法幀中本地變量表的長(zhǎng)度。 結(jié)果: 拋出StackOverflowError異常, 異常出現(xiàn)時(shí)輸出的堆棧深度相應(yīng)縮小。 首先, 對(duì)第一種情況進(jìn)行測(cè)試

public class JavaVMStackSOF {
    // 啟動(dòng)是添加VM Args: -Xss128k
    
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable ex) {
            System.out.println("stack length:" + oom.stackLength);
            throw ex;
        }
    }
}

運(yùn)行結(jié)果:

stack length:1407
Exception in thread "main" java.lang.StackOverflowError
    at com.ymj.jvm01.c3_code.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at com.ymj.jvm01.c3_code.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)

對(duì)于不同版本的Java虛擬機(jī)和不同的操作系統(tǒng), 棧容量最小值可能會(huì)有所限制, 這主要取決于操 作系統(tǒng)內(nèi)存分頁(yè)大小。 譬如上述方法中的參數(shù)-Xss128k可以正常用于32位Windows系統(tǒng)下的JDK 6, 但 是如果用于64位Windows系統(tǒng)下的JDK 11, 則會(huì)提示棧容量最小不能低于180K, 而在Linux下這個(gè)值則 可能是228K, 如果低于這個(gè)最小限制, HotSpot虛擬器啟動(dòng)時(shí)會(huì)給出如下提示:

The Java thread stack size specified is too small. Specify at least 228k

繼續(xù)驗(yàn)證第二種情況,為了多占局部變量表空間, 不得不定義一長(zhǎng)串變量,具體如代碼

public class JavaVMStackSOF2 {
    private static int stackLength = 0;
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
        unused6, unused7, unused8, unused9, unused10,
        unused11, unused12, unused13, unused14, unused15,
        unused16, unused17, unused18, unused19, unused20,
        unused21, unused22, unused23, unused24, unused25,
        unused26, unused27, unused28, unused29, unused30,
        unused31, unused32, unused33, unused34, unused35,
        unused36, unused37, unused38, unused39, unused40,
        unused41, unused42, unused43, unused44, unused45,
        unused46, unused47, unused48, unused49, unused50,
        unused51, unused52, unused53, unused54, unused55,
        unused56, unused57, unused58, unused59, unused60,
        unused61, unused62, unused63, unused64, unused65,
        unused66, unused67, unused68, unused69, unused70,
        unused71, unused72, unused73, unused74, unused75,
        unused76, unused77, unused78, unused79, unused80,
        unused81, unused82, unused83, unused84, unused85,
        unused86, unused87, unused88, unused89, unused90,
        unused91, unused92, unused93, unused94, unused95,
        unused96, unused97, unused98, unused99, unused100;
        stackLength ++;
        test();
        unused1 = unused2 = unused3 = unused4 = unused5 =
        unused6 = unused7 = unused8 = unused9 = unused10 =
        unused11 = unused12 = unused13 = unused14 = unused15 =
        unused16 = unused17 = unused18 = unused19 = unused20 =
        unused21 = unused22 = unused23 = unused24 = unused25 =
        unused26 = unused27 = unused28 = unused29 = unused30 =
        unused31 = unused32 = unused33 = unused34 = unused35 =
        unused36 = unused37 = unused38 = unused39 = unused40 =
        unused41 = unused42 = unused43 = unused44 = unused45 =
        unused46 = unused47 = unused48 = unused49 = unused50 =
        unused51 = unused52 = unused53 = unused54 = unused55 =
        unused56 = unused57 = unused58 = unused59 = unused60 =
        unused61 = unused62 = unused63 = unused64 = unused65 =
        unused66 = unused67 = unused68 = unused69 = unused70 =
        unused71 = unused72 = unused73 = unused74 = unused75 =
        unused76 = unused77 = unused78 = unused79 = unused80 =
        unused81 = unused82 = unused83 = unused84 = unused85 =
        unused86 = unused87 = unused88 = unused89 = unused90 =
        unused91 = unused92 = unused93 = unused94 = unused95 =
        unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }

    public static void main(String[] args) {
        try {
            test();
        } catch (Error e) {
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

運(yùn)行結(jié)果:

stack length:7983
Exception in thread "main" java.lang.StackOverflowError
    at com.ymj.jvm01.c3_code.JavaVMStackSOF2.test(JavaVMStackSOF2.java:33)
    at com.ymj.jvm01.c3_code.JavaVMStackSOF2.test(JavaVMStackSOF2.java:33)

實(shí)驗(yàn)結(jié)果表明: 無論是由于棧幀太大還是虛擬機(jī)棧容量太小, 當(dāng)新的棧幀內(nèi)存無法分配的時(shí)候, HotSpot虛擬機(jī)拋出的都是StackOverflowError異常。

3 運(yùn)行時(shí)常量池和方法區(qū)溢出

由于運(yùn)行時(shí)常量池是方法區(qū)的一部分, 所以這兩個(gè)區(qū)域的溢出測(cè)試可以放到一起進(jìn)行。前面曾經(jīng)提到HotSpot從 JDK 7開始逐步“去永久代”的計(jì)劃, 并在JDK 8中完全使用元空間來代替永久代的背景故事, 在此我們就以測(cè)試代碼來觀察一下, 使用“永久代”還是“元空間”來實(shí)現(xiàn)方法區(qū), 對(duì)程序有什么 實(shí)際的影響。
String::intern()是一個(gè)本地方法, 它的作用是如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的 字符串, 則返回代表池中這個(gè)字符串的String對(duì)象的引用; 否則, 會(huì)將此String對(duì)象包含的字符串添加到常量池中, 并且返回此String對(duì)象的引用。 在JDK 6或更早之前的HotSpot虛擬機(jī)中, 常量池都是分配在永久代中, 我們可以通過-XX: PermSize和-XX: MaxPermSize限制永久代的大小, 即可間接限制其中常量池的容量, 具體實(shí)現(xiàn)如代碼清單

3.1 運(yùn)行時(shí)常量池內(nèi)存溢出

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // VM Args: -XX:PermSize=6M -XX:MaxPermSize=6M

        // 使用Set保持著常量池引用, 避免Full GC回收常量池行為
        Set<String> set = new HashSet<String>();
        // 在short范圍內(nèi)足以讓6MB的PermSize產(chǎn)生OOM了
        short i = 0;
        while (true) {
            set.add(String.valueOf(i++).intern());
        }
    }
}

運(yùn)行結(jié)果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
at java.lang.String.intern(Native Method) 
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

從運(yùn)行結(jié)果中可以看到, 運(yùn)行時(shí)常量池溢出時(shí), 在OutOfMemoryError異常后面跟隨的提示信息 是“PermGen space”, 說明運(yùn)行時(shí)常量池的確是屬于方法區(qū)(即JDK 6的HotSpot虛擬機(jī)中的永久代) 的 一部分。
而使用JDK 7或更高版本的JDK來運(yùn)行這段程序并不會(huì)得到相同的結(jié)果, 無論是在JDK 7中繼續(xù)使 用-XX: MaxPermSize參數(shù)或者在JDK 8及以上版本使用-XX: MaxMeta-spaceSize參數(shù)把方法區(qū)容量同 樣限制在6MB, 也都不會(huì)重現(xiàn)JDK 6中的溢出異常, 循環(huán)將一直進(jìn)行下去, 永不停歇。 出現(xiàn)這種變 化, 是因?yàn)樽訨DK 7起, 原本存放在永久代的字符串常量池被移至Java堆之中, 所以在JDK 7及以上版本, 限制方法區(qū)的容量對(duì)該測(cè)試用例來說是毫無意義的。 這時(shí)候使用-Xmx參數(shù)限制最大堆到6MB就能 夠看到以下兩種運(yùn)行結(jié)果之一, 具體取決于哪里的對(duì)象分配時(shí)產(chǎn)生了溢出:

// OOM異常一: 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 
at java.base/java.lang.Integer.toString(Integer.java:440) at java.base/java.lang.String.valueOf(String.java:3058) 
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12) 
// OOM異常二: 
//根據(jù)Oracle官方文檔,默認(rèn)情況下,如果Java進(jìn)程花費(fèi)98%以上的時(shí)間執(zhí)行GC,并且每次只有不到2%的堆被恢復(fù),則JVM拋出 此錯(cuò)誤 
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded 
at java.lang.Integer.toString(Integer.java:401) 
at java.lang.String.valueOf(String.java:3099) 
at com.lagou.unit.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:17)

3.2 方法區(qū)內(nèi)存溢出

方法區(qū)的其他部分的內(nèi)容, 方法區(qū)的主要職責(zé)是用于存放類型的相關(guān)信息, 如類名、 訪問修飾符、 常量池、 字段描述、 方法描述等。 對(duì)于這部分區(qū)域的測(cè)試, 基本的思路是運(yùn)行時(shí)產(chǎn) 生大量的類去填滿方法區(qū), 直到溢出為止。
雖然直接使用Java SE API也可以動(dòng)態(tài)產(chǎn)生類(如反射時(shí)的 GeneratedConstructorAccessor和動(dòng)態(tài)代理等) , 但在本次實(shí)驗(yàn)中操作起來比較麻煩。 在代碼清單
借助CGLib使得方法區(qū)出現(xiàn)內(nèi)存溢出異常:

public class JavaMethodAreaOOM {

    static class OOMObject { }
    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[] args, MethodProxy proxy) throws ThrowException{
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}

在JDK 6中的運(yùn)行結(jié)果:

Caused by: java.lang.OutOfMemoryError: PermGen space 
at java.lang.ClassLoader.defineClass1(Native Method) 
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) 
at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more

方法區(qū)溢出也是一種常見的內(nèi)存溢出異常, 一個(gè)類如果要被垃圾收集器回收, 要達(dá)成的條件是比較苛刻的。 在經(jīng)常運(yùn)行時(shí)生成大量動(dòng)態(tài)類的應(yīng)用場(chǎng)景里, 就應(yīng)該特別關(guān)注這些類的回收狀況。 這類場(chǎng) 景除了之前提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語(yǔ)言外, 常見的還有: 大量JSP或動(dòng)態(tài)產(chǎn)生JSP 文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯
為Java類) 、 基于OSGi的應(yīng)用(即使是同一個(gè)類文件, 被不同 的加載器加載也會(huì)視為不同的類) 等。 在JDK 8以后, 永久代便完全退出了歷史舞臺(tái), 元空間作為其替代者登場(chǎng)。 在默認(rèn)設(shè)置下, 前面列舉的那些正常的動(dòng)態(tài)創(chuàng)建新類型的測(cè)試用例已經(jīng)很難再迫使虛擬機(jī)產(chǎn)生方法區(qū)的溢出異常了。 不過 為了讓使用者有預(yù)防實(shí)際應(yīng)用里出現(xiàn)類似于代碼清單2-9那樣的破壞性的操作, HotSpot還是提供了一 些參數(shù)作為元空間的防御措施, 主要包括:
-XX: MaxMetaspaceSize: 設(shè)置元空間最大值, 默認(rèn)是-1, 即不限制, 或者說只受限于本地內(nèi)存 大小。
-XX: MetaspaceSize: 指定元空間的初始空間大小, 以字節(jié)為單位, 達(dá)到該值就會(huì)觸發(fā)垃圾收集 進(jìn)行類型卸載,同時(shí)收集器會(huì)對(duì)該值進(jìn)行調(diào)整: 如果釋放了大量的空間, 就適當(dāng)降低該值; 如果釋放 了很少的空間, 那么在不超過-XX: MaxMetaspaceSize(如果設(shè)置了的話) 的情況下, 適當(dāng)提高該值。
-XX: MinMetaspaceFreeRatio: 作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可減少因?yàn)樵臻g不足導(dǎo)致的垃圾收集的頻率。 類似的還有-XX: Max-MetaspaceFreeRatio, 用于控制最大的元空間剩余容量的百分比。

4 直接內(nèi)存溢出

直接內(nèi)存(Direct Memory) 的容量大小可通過-XX: MaxDirectMemorySize參數(shù)來指定, 如果不去指定, 則默認(rèn)與Java堆最大值(由-Xmx指定) 一致, 越過了DirectByteBuwer類直接通 過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法指定只有引導(dǎo)類加載器才會(huì)返回實(shí)例, 體現(xiàn)了設(shè)計(jì)者希望只有虛擬機(jī)標(biāo)準(zhǔn)類庫(kù)里面的類才能使用Unsafe的功能, 在JDK 10時(shí)才將Unsafe 的部分功能通過VarHandle開放給外部使用) , 因?yàn)殡m然使用DirectByteBuwer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常, 但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存, 而是通過計(jì)算得知內(nèi)存無法分配就會(huì) 在代碼里手動(dòng)拋出溢出異常, 真正申請(qǐng)分配內(nèi)存的方法是Unsafe::allocateMemory()

public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    // VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

運(yùn)行結(jié)果:

Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at com.ymj.jvm01.c3_code.DirectMemoryOOM.main(DirectMemoryOOM.java:22)

由直接內(nèi)存導(dǎo)致的內(nèi)存溢出, 一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見有什么明顯的異常 情況, 如果發(fā)現(xiàn)內(nèi)存溢出之后產(chǎn)生的Dump文件很小, 而程序中又直接或間接使用了 DirectMemory(典型的間接使用就是NIO) ,那就可以考慮重點(diǎn)檢查一下直接內(nèi)存方面的原因了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容