官术网_书友最值得收藏!

2.4 實戰:OutOfMemoryError異常

在《Java虛擬機規范》的規定里,除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(下文稱OOM)異常的可能,本節將通過若干實例來驗證異常實際發生的代碼場景(代碼清單2-3~2-9),并且將初步介紹若干最基本的與自動內存管理子系統相關的HotSpot虛擬機參數。

本節實戰的目的有兩個:第一,通過代碼驗證《Java虛擬機規范》中描述的各個運行時區域儲存的內容;第二,希望讀者在工作中遇到實際的內存溢出異常時,能根據異常的提示信息迅速得知是哪個區域的內存溢出,知道怎樣的代碼可能會導致這些區域內存溢出,以及出現這些異常后該如何處理。

本節代碼清單開頭都注釋了執行時需要設置的虛擬機啟動參數(注釋中“VM Args”后面跟著的參數),這些參數對實驗的結果有直接影響,請讀者調試代碼的時候不要忽略掉。如果讀者使用控制臺命令來執行程序,那直接跟在Java命令之后書寫就可以。如果讀者使用Eclipse,則可以參考圖2-4在Debug/Run頁簽中的設置,其他IDE工具均有類似的設置。

圖2-4 在Eclipse的Debug頁簽中設置虛擬機參數

本節所列的代碼均由筆者在基于OpenJDK 7中的HotSpot虛擬機上進行過實際測試,如無特殊說明,對其他OpenJDK版本也應當適用。不過讀者需意識到內存溢出異常與虛擬機本身的實現細節密切相關,并非全是Java語言中約定的公共行為。因此,不同發行商、不同版本的Java虛擬機,其需要的參數和程序運行的結果都很可能會有所差別。

2.4.1 Java堆溢出

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

代碼清單2-3中限制Java堆的大小為20MB,不可擴展(將堆的最小值-Xms參數與最大值-Xmx參數設置為一樣即可避免堆自動擴展),通過參數-XX:+HeapDumpOnOutOf-MemoryError可以讓虛擬機在出現內存溢出異常的時候Dump出當前的內存堆轉儲快照以便進行事后分析關于堆轉儲快照文件分析方面的內容,可參見第4章。

代碼清單2-3 Java堆內存溢出異常測試

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author zzm
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}

運行結果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]

Java堆內存的OutOfMemoryError異常是實際應用中最常見的內存溢出異常情況。出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟隨進一步提示“Java heap space”。

要解決這個內存區域的異常,常規的處理方法是首先通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析。第一步首先應確認內存中導致OOM的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。圖2-5顯示了使用Eclipse Memory Analyzer打開的堆轉儲快照文件。

如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈,找到泄漏對象是通過怎樣的引用路徑、與哪些GC Roots相關聯,才導致垃圾收集器無法回收它們,根據泄漏對象的類型信息以及它到GC Roots引用鏈的信息,一般可以比較準確地定位到這些對象創建的位置,進而找出產生內存泄漏的代碼的具體位置。

圖2-5 使用Eclipse Memory Analyzer打開的堆轉儲快照文件

如果不是內存泄漏,換句話說就是內存中的對象確實都是必須存活的,那就應當檢查Java虛擬機的堆參數(-Xmx與-Xms)設置,與機器的內存對比,看看是否還有向上調整的空間。再從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長、存儲結構設計不合理等情況,盡量減少程序運行期的內存消耗。

以上是處理Java堆內存問題的簡略思路,處理這些問題所需要的知識、工具與經驗是后面三章的主題,后面我們將會針對具體的虛擬機實現、具體的垃圾收集器和具體的案例來進行分析,這里就先暫不展開。

2.4.2 虛擬機棧和本地方法棧溢出

由于HotSpot虛擬機中并不區分虛擬機棧和本地方法棧,因此對于HotSpot來說,-Xoss參數(設置本地方法棧大小)雖然存在,但實際上是沒有任何效果的,棧容量只能由-Xss參數來設定。關于虛擬機棧和本地方法棧,在《Java虛擬機規范》中描述了兩種異常:

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

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

《Java虛擬機規范》明確允許Java虛擬機實現自行選擇是否支持棧的動態擴展,而HotSpot虛擬機的選擇是不支持擴展,所以除非在創建線程申請內存時就因無法獲得足夠內存而出現OutOfMemoryError異常,否則在線程運行時是不會因為擴展而導致內存溢出的,只會因為棧容量無法容納新的棧幀而導致StackOverflowError異常。

為了驗證這點,我們可以做兩個實驗,先將實驗范圍限制在單線程中操作,嘗試下面兩種行為是否能讓HotSpot虛擬機產生OutOfMemoryError異常:

·使用-Xss參數減少棧內存容量。

結果:拋出StackOverflowError異常,異常出現時輸出的堆棧深度相應縮小。

·定義了大量的本地變量,增大此方法幀中本地變量表的長度。

結果:拋出StackOverflowError異常,異常出現時輸出的堆棧深度相應縮小。

首先,對第一種情況進行測試,具體如代碼清單2-4所示。

代碼清單2-4 虛擬機棧和本地方法棧測試(作為第1點測試程序)

/**
 * VM Args:-Xss128k
 * @author zzm
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

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

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

運行結果:

stack length:2402
Exception in thread "main" java.lang.StackOverflowError
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:20)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:21)
……后續異常堆棧信息省略

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

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

我們繼續驗證第二種情況,這次代碼就顯得有些“丑陋”了,為了多占局部變量表空間,筆者不得不定義一長串變量,具體如代碼清單2-5所示。

代碼清單2-5 虛擬機棧和本地方法棧測試(作為第2點測試程序)

/**
 * @author zzm
 */
public class JavaVMStackSOF {
    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;
        }
    }
}

運行結果:

stack length:5675
Exception in thread "main" java.lang.StackOverflowError
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:27)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
……后續異常堆棧信息省略

實驗結果表明:無論是由于棧幀太大還是虛擬機棧容量太小,當新的棧幀內存無法分配的時候,HotSpot虛擬機拋出的都是StackOverflowError異常。可是如果在允許動態擴展棧容量大小的虛擬機上,相同代碼則會導致不一樣的情況。譬如遠古時代的Classic虛擬機,這款虛擬機可以支持動態擴展棧內存的容量,在Windows上的JDK 1.0.2運行代碼清單2-5的話(如果這時候要調整棧容量就應該改用-oss參數了),得到的結果是:

stack length:3716
java.lang.OutOfMemoryError
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:27)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
    at org.fenixsoft.oom. JavaVMStackSOF.leak(JavaVMStackSOF.java:28)
……后續異常堆棧信息省略

可見相同的代碼在Classic虛擬機中成功產生了OutOfMemoryError而不是StackOver-flowError異常。如果測試時不限于單線程,通過不斷建立線程的方式,在HotSpot上也是可以產生內存溢出異常的,具體如代碼清單2-6所示。但是這樣產生的內存溢出異常和棧空間是否足夠并不存在任何直接的關系,主要取決于操作系統本身的內存使用狀態。甚至可以說,在這種情況下,給每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。

原因其實不難理解,操作系統分配給每個進程的內存是有限制的,譬如32位Windows的單個進程最大內存限制為2GB。HotSpot虛擬機提供了參數可以控制Java堆和方法區這兩部分的內存的最大值,那剩余的內存即為2GB(操作系統限制)減去最大堆容量,再減去最大方法區容量,由于程序計數器消耗內存很小,可以忽略掉,如果把直接內存和虛擬機進程本身耗費的內存也去掉的話,剩下的內存就由虛擬機棧和本地方法棧來分配了。因此為每個線程分配到的棧內存越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡,代碼清單2-6演示了這種情況。

代碼清單2-6 創建線程導致內存溢出異常

/**
 * VM Args:-Xss2M (這時候不妨設大些,請在32位系統下運行)
 * @author zzm
 */
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

注意 重點提示一下,如果讀者要嘗試運行上面這段代碼,記得要先保存當前的工作,由于在Windows平臺的虛擬機中,Java的線程是映射到操作系統的內核線程上關于虛擬機線程實現方面的內容可以參考本書第12章。,無限制地創建線程會對操作系統帶來很大壓力,上述代碼執行時有很高的風險,可能會由于創建線程數量過多而導致操作系統假死。

在32位操作系統下的運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

出現StackOverflowError異常時,會有明確錯誤堆棧可供分析,相對而言比較容易定位到問題所在。如果使用HotSpot虛擬機默認參數,棧深度在大多數情況下(因為每個方法壓入棧的幀大小并不是一樣的,所以只能說大多數情況下)到達1000~2000是完全沒有問題,對于正常的方法調用(包括不能做尾遞歸優化的遞歸調用),這個深度應該完全夠用了。但是,如果是建立過多線程導致的內存溢出,在不能減少線程數量或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。這種通過“減少內存”的手段來解決內存溢出的方式,如果沒有這方面處理經驗,一般比較難以想到,這一點讀者需要在開發32位系統的多線程應用時注意。也是由于這種問題較為隱蔽,從JDK 7起,以上提示信息中“unable to create native thread”后面,虛擬機會特別注明原因可能是“possibly out of memory or process/resource limits reached”。

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

由于運行時常量池是方法區的一部分,所以這兩個區域的溢出測試可以放到一起進行。前面曾經提到HotSpot從JDK 7開始逐步“去永久代”的計劃,并在JDK 8中完全使用元空間來代替永久代的背景故事,在此我們就以測試代碼來觀察一下,使用“永久代”還是“元空間”來實現方法區,對程序有什么實際的影響。

String::intern()是一個本地方法,它的作用是如果字符串常量池中已經包含一個等于此String對象的字符串,則返回代表池中這個字符串的String對象的引用;否則,會將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用。在JDK 6或更早之前的HotSpot虛擬機中,常量池都是分配在永久代中,我們可以通過-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可間接限制其中常量池的容量,具體實現如代碼清單2-7所示,請讀者測試時首先以JDK 6來運行代碼。

代碼清單2-7 運行時常量池導致的內存溢出異常

/**
 * VM Args:-XX:PermSize=6M -XX:MaxPermSize=6M
 * @author zzm
 */
public class RuntimeConstantPoolOOM {

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

運行結果:

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)

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

而使用JDK 7或更高版本的JDK來運行這段程序并不會得到相同的結果,無論是在JDK 7中繼續使用-XX:MaxPermSize參數或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize參數把方法區容量同樣限制在6MB,也都不會重現JDK 6中的溢出異常,循環將一直進行下去,永不停歇正常情況下是永不停歇的,如果機器內存緊張到連幾MB的Java堆都擠不出來的這種極端情況就不討論了。。出現這種變化,是因為自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版本,限制方法區的容量對該測試用例來說是毫無意義的。這時候使用-Xmx參數限制最大堆到6MB就能夠看到以下兩種運行結果之一,具體取決于哪里的對象分配時產生了溢出:

// 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異常二:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.base/java.util.HashMap.resize(HashMap.java:699)
    at java.base/java.util.HashMap.putVal(HashMap.java:658)
    at java.base/java.util.HashMap.put(HashMap.java:607)
    at java.base/java.util.HashSet.add(HashSet.java:220)
    at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)

關于這個字符串常量池的實現在哪里出現問題,還可以引申出一些更有意思的影響,具體見代碼清單2-8所示。

代碼清單2-8 String.intern()返回引用的測試

public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        String str1 = new StringBuilder("計算機").append("軟件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

這段代碼在JDK 6中運行,會得到兩個false,而在JDK 7中運行,會得到一個true和一個false。產生差異的原因是,在JDK 6中,intern()方法會把首次遇到的字符串實例復制到永久代的字符串常量池中存儲,返回的也是永久代里面這個字符串實例的引用,而由StringBuilder創建的字符串對象實例在Java堆上,所以必然不可能是同一個引用,結果將返回false。

而JDK 7(以及部分其他虛擬機,例如JRockit)的intern()方法實現就不需要再拷貝字符串的實例到永久代了,既然字符串常量池已經移到Java堆中,那只需要在常量池里記錄一下首次出現的實例引用即可,因此intern()返回的引用和由StringBuilder創建的那個字符串實例就是同一個。而對str2比較返回false,這是因為“java”它是在加載sun.misc.Version這個類的時候進入常量池的。本書第2版并未解釋java這個字符串此前是哪里出現的,所以被批評“挖坑不填了”(無奈地攤手)。如讀者感興趣是如何找出來的,可參考Red-naxelaFX的知乎回答(https://www.zhihu.com/question/51102308/answer/124441115)。這個字符串在執行String-Builder.toString()之前就已經出現過了,字符串常量池中已經有它的引用,不符合intern()方法要求“首次遇到”的原則,“計算機軟件”這個字符串則是首次出現的,因此結果返回true。

我們再來看看方法區的其他部分的內容,方法區的主要職責是用于存放類型的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對于這部分區域的測試,基本的思路是運行時產生大量的類去填滿方法區,直到溢出為止。雖然直接使用Java SE API也可以動態產生類(如反射時的GeneratedConstructorAccessor和動態代理等),但在本次實驗中操作起來比較麻煩。在代碼清單2-9里筆者借助了CGLibCGLib開源項目:http://cglib.sourceforge.net/。直接操作字節碼運行時生成了大量的動態類。

值得特別注意的是,我們在這個例子中模擬的場景并非純粹是一個實驗,類似這樣的代碼確實可能會出現在實際應用中:當前的很多主流框架,如Spring、Hibernate對類進行增強時,都會使用到CGLib這類字節碼技術,當增強的類越多,就需要越大的方法區以保證動態生成的新類型可以載入內存。另外,很多運行于Java虛擬機上的動態語言(例如Groovy等)通常都會持續創建新類型來支撐語言的動態性,隨著這類動態語言的流行,與代碼清單2-9相似的溢出場景也越來越容易遇到。

代碼清單2-9 借助CGLib使得方法區出現內存溢出異常

/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * @author zzm
 */
public class JavaMethodAreaOOM {

    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 Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}

在JDK 7中的運行結果:

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

方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收,要達成的條件是比較苛刻的。在經常運行時生成大量動態類的應用場景里,就應該特別關注這些類的回收狀況。這類場景除了之前提到的程序使用了CGLib字節碼增強和動態語言外,常見的還有:大量JSP或動態產生JSP文件的應用(JSP第一次運行時需要編譯為Java類)、基于OSGi的應用(即使是同一個類文件,被不同的加載器加載也會視為不同的類)等。

在JDK 8以后,永久代便完全退出了歷史舞臺,元空間作為其替代者登場。在默認設置下,前面列舉的那些正常的動態創建新類型的測試用例已經很難再迫使虛擬機產生方法區的溢出異常了。不過為了讓使用者有預防實際應用里出現類似于代碼清單2-9那樣的破壞性的操作,HotSpot還是提供了一些參數作為元空間的防御措施,主要包括:

·-XX:MaxMetaspaceSize:設置元空間最大值,默認是-1,即不限制,或者說只受限于本地內存大小。

·-XX:MetaspaceSize:指定元空間的初始空間大小,以字節為單位,達到該值就會觸發垃圾收集進行類型卸載,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過-XX:MaxMetaspaceSize(如果設置了的話)的情況下,適當提高該值。

·-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空間剩余容量的百分比,可減少因為元空間不足導致的垃圾收集的頻率。類似的還有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空間剩余容量的百分比。

2.4.4 本機直接內存溢出

直接內存(Direct Memory)的容量大小可通過-XX:MaxDirectMemorySize參數來指定,如果不去指定,則默認與Java堆最大值(由-Xmx指定)一致,代碼清單2-10越過了DirectByteBuffer類直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法指定只有引導類加載器才會返回實例,體現了設計者希望只有虛擬機標準類庫里面的類才能使用Unsafe的功能,在JDK 10時才將Unsafe的部分功能通過VarHandle開放給外部使用),因為雖然使用DirectByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時并沒有真正向操作系統申請分配內存,而是通過計算得知內存無法分配就會在代碼里手動拋出溢出異常,真正申請分配內存的方法是Unsafe::allocateMemory()。

代碼清單2-10 使用unsafe分配本機內存

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * @author zzm
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    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);
        }
    }
}

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)

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

主站蜘蛛池模板: 绥德县| 嵩明县| 福清市| 宜宾县| 阿鲁科尔沁旗| 鄂托克前旗| 安西县| 嵊泗县| 察雅县| 繁昌县| 眉山市| 伊宁县| 达拉特旗| 塔河县| 扬州市| 宣城市| 大连市| 蓬安县| 库尔勒市| 杭锦后旗| 平定县| 哈巴河县| 大英县| 高州市| 临桂县| 武陟县| 金寨县| 沙雅县| 尼玛县| 晋江市| 新巴尔虎右旗| 犍为县| 临清市| 无锡市| 兰州市| 政和县| 炉霍县| 介休市| 通渭县| 清徐县| 衡阳县|