深入理解Java虛擬機(三):實戰OutOfMemoryError異常

引言

在系列文章的第一篇中,我們就瞭解到,除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(下文稱OOM)異常的可能。本篇博客將通過代碼示例來再現異常情景,最後也會總結若干最基本的與自動內存管理子系統相關的HotSpot虛擬機參數。

異常場景

首先,我們可以在開發工具中設置一些JVM運行參數,如最大堆、最小堆等,這對示例代碼的結果會有直接影響。以IDEA開發工具來說,我們在Edit Configurations對話框中的VM Options一項中進行設置,如下:
在這裏插入圖片描述

1. Java堆溢出

Java堆用於儲存對象實例,我們只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼隨着對象數量的增加,總容量觸及最大堆的容量限制後就會產生內存溢出異常。

示例代碼:
/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @Description : java堆溢出
 * @Author : huzhiting
 * @Date: 2020-06-04 16:59
 */
public class HeapOOM {

    private static class OOMObject{}

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true){
            list.add(new OOMObject());
        }
    }
}
運行結果:

在這裏插入圖片描述

2. 虛擬機棧和本地方法棧溢出

由於HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,因此對於HotSpot來說,-Xoss參數(設置本地方法棧大小)雖然存在,但實際上是沒有任何效果的,棧容量只能由-Xss參數來設定。

回顧一下可能發生的兩種異常情況:

(1)如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。

(2)如果虛擬機的棧內存允許動態擴展,當擴展棧容量無法申請到足夠的內存時,將拋出OutOfMemoryError異常。

而HotSpot虛擬機的選擇是不支持擴展,所以除非在創建線程申請內存時就因無法獲得足夠內存而出現OutOfMemoryError異常,否則在線程運行時是不會因爲擴展而導致內存溢出的。

示例代碼:
/**
 * VM Args:-Xss128K
 * @Description : java棧溢出
 * @Author : huzhiting
 * @Date: 2020-06-04 17:48
 */
public class JavaVMStackSOF {

    private int stackLength=1;

    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

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

在這裏插入圖片描述

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

String::intern()是一個本地方法,它的作用是如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象的引用;否則,會將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。

在JDK 6或更早之前的HotSpot虛擬機中,常量池都是分配在永久代中,我們可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量。

示例代碼:
/**
 * VM args: -XX:PermSize=6M -XX:MaxPermSize=6M
 * @Description : 運行時常量池溢出
 * @Author : huzhiting
 * @Date: 2020-06-04 18:16
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        short i =0;
        while (true){
            set.add(String.valueOf(i++).intern());
        }
    }
}
運行結果:

在這裏插入圖片描述
從運行結果中可以看到,運行時常量池溢出時,在OutOfMemoryError異常後面跟隨的提示信息是“PermGen space”,說明運行時常量池的確是屬於方法區(即JDK 6的HotSpot虛擬機中的永久代)的一部分。

而使用JDK 7或更高版本的JDK來運行這段程序並不會得到相同的結果,無論是在JDK 7中繼續使用-XX:MaxPermSize參數或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數把方法區容量同樣限制在6MB,也都不會重現JDK 6中的溢出異常。

出現這種變化,是因爲自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中。這時候使用-Xmx參數限制最大堆到6MB就能夠看到以下運行結果:

在這裏插入圖片描述
在JDK 8以後,永久代便完全退出了歷史舞臺,元空間作爲其替代者登場。所以在默認設置下,虛擬機很難產生方法區溢出的異常現象,這裏就不再用代碼進行測試了。

爲了避免一些破壞性操作,HotSpot提供了一些參數作爲元空間的防禦措施,主要包括:

  • -XX:MaxMetaspaceSize:設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小。
  • -XX:MetaspaceSize:指定元空間的初始空間大小,以字節爲單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過-XX:MaxMetaspaceSize(如果設置了的話)的情況下,適當提高該值。
  • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之後控制最小的元空間剩餘容量的百分比,可減少因爲元空間不足導致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用於控制最大的元空間剩餘容量的百分比。
4. 本機直接內存溢出

直接內存(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize參數來指定,如果不去指定,則默認與Java堆最大值(由-Xmx指定)一致。

溢出運行結果如下:
在這裏插入圖片描述

總結

在有了一定工作經驗的基礎上,相信本篇博客的核心OutOfMemoryError也是不少人都遇到過的。

線上如果出現此類問題,一定是劃定爲線上事故了。所以,作爲一名有經驗的開發者,在遇到該類問題如何快速定位問題,如何性能調優等等,都是必須要掌握的。

本文只是通過一些示例代碼列舉異常現象,後續還會針對JVM參數及問題排查過程進行單獨總結,持續學習中…

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