Java虛擬機(4)OutOfMemoryError異常

在《Java虛擬機規範》的規定裏,除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能。

Java堆溢出

Java堆內存的OutOfMemoryError異常是實際應用中最常見的內存溢出異常情況。

產生原因

Java堆用於儲存對象實例,不斷地創建對象,總容量觸及最大堆的容量限制後就會產生內存溢出異常。

處理方法

首先通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,確認是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。

  • 內存泄漏
  1. 通過工具查看泄漏對象到GC Roots的引用鏈,找到泄漏對象是通過怎樣的引用路徑、與哪些GCRoots相關聯,才導致垃圾收集器無法回收它們;
  2. 根據泄漏對象的類型信息以及它到GC Roots引用鏈的信息, 定位到這些對象創建的位置, 找出產生內存泄漏的代碼 。
  • 不是內存泄漏(內存中的對象確實都是必須存活的)
  1. 檢查Java虛擬機的堆參數(-Xmx與-Xms)設置,與機器的內存對比, 是否還有向上調整的空間;
  2. 代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長、存儲結構設計不合理等情況,儘量減少程序運行期的內存消耗。

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

關於虛擬機棧和本地方法棧,在《Java虛擬機規範》中描述了兩種異常:

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

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

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

HotSpot從JDK 7開始逐步“去永久代”的計劃,並在JDK 8中完全使用元空間來代替永久代,原本存放在永久代的字符串常量池被移至Java堆之中。

用以下一段代碼來講述區別:

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。

原因是,在JDK6中,intern()方法會把首次遇到的字符串實例複製到永久代的字符串常量池中存儲,返回的也是永久代裏面這個字符串實例的引用,而由StringBuilder創建的字符串對象實例在Java堆上,所以必然不可能是同一個引用,結果將返回false。

而JDK 7(以及部分其他虛擬機,例如JRockit)的intern()方法實現就不需要再拷貝字符串的實例到永久代了。 字符串常量池已經移到Java堆中, 只需要在常量池裏記錄一下首次出現的實例引用即可,因此intern()返回的引用和由StringBuilder創建的那個字符串實例就是同一個。

對str2比較返回false,這是因爲"java"這個字符串在執行StringBuilder.toString()之前就已經出現過了,字符串常量池中已經有它的引用,不符合intern()方法要求“首次遇到”的原則。而“計算機軟件”這個字符串則是首次出現的,因此結果返回true。

本機直接內存溢出

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

歡迎點贊/評論,你們的贊同和鼓勵是我寫作的最大動力!

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