深入理解java虛擬機——OutOfMemoryError異常

除了程序計數器外都有可能發生OutOfMemoryError異常的可能。

1.java堆溢出

不斷創建對象,並保證GC Root到對象之間有可達路徑來避免垃圾回收機制清楚這些對象,在達到最大堆的容量限制後就會產生內存溢出異常。

堆大小設置爲20MB並在出現內存溢出異常時Dump出當前的內存堆轉儲快照

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

當出現java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟着進一步提示“java heap space”

解決:根據內存映像分析工具分析是內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。

如果是內存泄漏就通過工具查看泄露對象到GC Roots的引用鏈。定位出泄漏代碼。

如果不存在泄漏,應當檢查虛擬機的堆參數(-Xms與-Xmx),看是否可以調大。

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

HotSpot虛擬機中並不區分虛擬機棧和和本地方法棧,因此-Xoss參數(設置本地方法棧大小)存在,但實際上是無效的,棧容量只由-Xss參數設定。

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

如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。
如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這兩種情況存在着一些互相重疊的地方:當棧空間無法繼續分配時,到底是內存太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。在單線程的操作中,無論是由於棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是 StackOverflowError 異常,而不會得到 OutOfMemoryError 異常。而在多線程環境下,則會拋出 OutOfMemoryError 異常。

原因其實不難理解,操作系統分配給每個進程的內存是有限制的,譬如 32 位的 Windows 限制爲 2GB。虛擬機提供了參數來控制 Java 堆和方法區的這兩部分內存的最大值。剩餘的內存爲 2GB(操作系統限制)減去 Xmx(最大堆容量),再減去 MaxPermSize(最大方法區容量),程序計數器消耗內存很小,可以忽略掉。如果虛擬機進程本身耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法棧“瓜分”了。每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡。
遇到OOM這時候我們應該怎麼做:如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換 64 位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。如果沒有這方面的經驗,這種通過“減少內存”的手段來解決內存溢出的方式會比較難以想到。

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

在jdk1.6及之前的版本中,由於常量池分配在永久代內,我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量。

通過String.intern()方法試驗。

jdk1.6環境通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,會產生java.lang.OutOfMemoryError: PermGen space錯誤。

jdk1.7環境通過-Xms和 -Xmx限制堆大小,會產生 java.lang.OutOfMemoryError: Java heap space錯誤。

intern方法:當調用該方法時,會在運行時常量池查看有無該字符串,如果有的話,直接返回該引用(這個字符串在運行時常量池中的地址);如果沒有的話,在運行時常量池中存放一份數據,並返回該字符串在運行時常量池中的地址。這都是在JDK1.6及之前發生的事情。如果是1.7之後,如果發現在運行時常量池中沒有的話,在運行時常量池中存放的是一份這個字符串String的引用(即這個String對象在堆中的地址),並返回該引用的值。

當class文件被加載(load)到JVM時會將常量池中的內容存放在運行時常量池(在perm區中,即永久代)中。上面 這一句只是針對JDK1.6及之前的版本適用。JDK1.7之後對方法區進行了一些優化。1.7之後將運行時常量池移到了堆中()。原因,Perm 區是一個類靜態的區域,主要存儲一些加載類的信息,常量池,方法片段等內容,默認大小隻有4M。一旦超過範圍,會直接產生java.lang.OutOfMemoryError: PermGen space錯誤。 

方法區溢出也是一種常見的內存溢出異常,一個類要被垃圾器回收掉,判定條件是比較嚴苛的。在經常動態產生大量Class的應用中(會產生java.lang.OutOfMemoryError: PermGen space錯誤),需要特別注意類的回收狀況。這類場景除了上面提到的程序使用了CGLib字節碼增強和動態語言之外,常見的還有:大量JSP或者動態產生JSP文件的應用、基於OSGi的應用等。

4.本機直接內存溢出

DirectMemory容量可以通過-XX:MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值(-Xmx指定)一樣。代碼清單越過了DirectByteBuffer類,直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe方法限制了只有引導類加載器纔會返回實例,也就是設計者希望只有rt.jar中的類才能使用Unsafe的功能)。因爲,雖然使用DirectByteBuffer分配內存也會拋出內存異常,但它拋出異常時並沒有真正向操作系統申請內存分配,而是通過計算得知內存無法分配,於是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory.

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

由DirectMemory導致的內存溢出,一個明顯的特徵是在Heap Dump文件中不會看見明顯的異常,如果讀者發現OOM之後Dump文件很小,而程序中又直接或者間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

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