1、這些異常你是否遇到過?
正式開講之前,先羅列一下所知的 OutOfMemoryError (簡稱 OOM)異常,看看這些異常工作中你是否也遇到過?
Java 堆內存溢出:java.lang.OutOfMemoryError: Java heap space
垃圾回收內存溢出:java.lang.OutOfMemoryError: GC overhead limit exceeded
方法區溢出:java.lang.OutOfMemoryError: PermGen space
Metaspace 內存溢出:java.lang.OutOfMemoryError: Metaspace
直接內存內存溢出:java.lang.OutOfMemoryError: Direct buffer memory
棧內存溢出:java.lang.StackOverflowError
創建本地線程內存溢出:java.lang.OutOfMemoryError: Unable to create new native thread
數組超限內存溢出:java.lang.OutOfMemoryError:Requested array size exceeds VM limit
在實際工作中,若真遇到了上面羅列的這些內存溢出的異常,你是否能夠根據異常提示迅速定位是哪兒出了問題,並是否能夠鏟除這些問題呢?
希望通過此篇分享,盡量能夠讓大家了解每個異常發生的場景,並能夠掌握每個異常場景的應對之策。
如上圖示意,按照內存共享來划分 JVM 內存,主要划分為線程共享內存區域(堆、方法區)、線程私有內存區域(程序計數器、虛擬機棧、本地方法棧)、直接內存。而在《Java 虛擬機規范》的規定里,除了程序計數器外,虛擬機內存的其它幾個運行時區域都可能發生 OOM 異常,接下來通過代碼來剖析一下各種 OutOfMemoryError(OOM)的場景。
2、實戰:OutOfMemoryError 異常
場景一 java.lang.OutOfMemoryError: Java heap space
/** * VM options:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { public static void main(String[] args) { byte[] bytes = new byte[20 * 1024 * 1024]; System.out.println(bytes); } }
代碼很簡單,創建一個字節數組對象,要分配 20M 的空間。若在運行程序時指定 VM 參數:
- 通過參數 -Xms10m -Xmx10m 將堆的最小值與最大值都設置為 10M,即限制 Java 堆的大小為 10MB,並且避免堆自動擴展;
- 通過參數 -XX:+HeapDumpOnOutOf-MemoryError 讓虛擬機在出現內存溢出異常的時候 Dump 出當前的內存堆轉儲快照以便進行事后分析。
指定 VM options 后的運行結果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid35115.hprof ... Heap dump file created [1033561 bytes in 0.005 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at HeapOOM.main(HeapOOM.java:7)
為什么呢?簡單解釋原因,-Xms10m -Xmx10m 限制了堆的最大值為 10M,而 new byte[20 * 1024 * 1024] 需要 20M 的空間,則堆內存明顯不夠,則直接導致 OOM。
面對此種異常,常規解決思路:
- 要檢查一下代碼是否存在優化的空間;
- 依據內存溢出時的快照文件 xx.hprof 來判斷是否存在內存泄露,不需要的對象有沒有被回收掉;
- 調節虛擬機的堆參數(-Xms -Xmx),適當調大堆內存。
場景二 java.lang.OutOfMemoryError: GC overhead limit exceeded
/** * VM options:-Xmx6m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class GirlFriend { } public static void main(String[] args) { List<GirlFriend> list = new ArrayList<GirlFriend>(); while (true) { list.add(new GirlFriend()); } } }
代碼很簡單,一直往集合中加入新創建的對象(虛妄的單身狗生活:一直創建女朋友對象。)
若在運行程序時指定 VM 參數:
- 通過參數 -Xmx6m 將堆的最大值設置為 6M;
- 通過參數 -XX:+HeapDumpOnOutOf-MemoryError 讓虛擬機在出現內存溢出異常的時候 Dump 出當前的內存堆轉儲快照以便進行事后分析。
指定 VM options 后的運行結果:
java.lang.OutOfMemoryError: GC overhead limit exceeded Dumping heap to java_pid35304.hprof ... Heap dump file created [12557270 bytes in 0.082 secs] Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at HeapOOM.main(HeapOOM.java:16)
為什么呢?嘗試解讀一下原英文解釋。
The parallel(concurrent) collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown.
大概意思應用程序在垃圾收集上花費了太多時間,但是卻沒有什么卵用,默認超過 98% 的時間用來做GC卻回收了不到2%的內存時將會拋出 OutOfMemoryError 異常。
面對此種異常,常規解決思路:
- 程序啟動添加 JVM 參數 -XX:-UseGCOverheadLimit 推遲異常報出,而並非徹底解決了問題;
- 好好分析快照文件 xx.hprof,排除代碼問題,若確實堆內存不足,通過參數 -Xmx 適度調整堆內存大小。
場景三 java.lang.OutOfMemoryError: PermGen space
首先來解釋一下 PermGen space 的用處,主要用來存儲每個類的信息,例如:類加載器引用、運行時常量池(所有常量、字段引用、方法引用、屬性)、字段(Field)數據、方法(Method)數據、方法代碼、方法字節碼等等。
當出現 java.lang.OutOfMemoryError: PermGen space 異常時,要能夠知道可能是由於太多的類或者太大的類被加載到方法區導致的。
解決方案:可以根據具體情況采用 -XX:MaxPermSize=64m 參數來加大分配的內存進行解決。
場景四 java.lang.OutOfMemoryError: Metaspace
在 JDK6、7 還能夠見到java.lang.OutOfMemoryError: PermGen space異常的蹤影,而在 JDK8 以后,永久代便完全退出了歷史舞台,元空間作為其替代者登場,在默認參數設置下,已經很難再迫使虛擬機產生上面所描述的異常了。不過 java.lang.OutOfMemoryError: Metaspace 異常偶爾就會碰到了。
java.lang.OutOfMemoryError: Metaspace(元空間的溢出),為什么會出現這個異常?元空間大小的要求取決於加載的類的數量以及這種類聲明的大小,所以主要原因很可能是太多的類或太大的類加載到元空間導致的。
解決方案:
- 優化參數配置,適度調大該值 -XX:MaxMetaspaceSize;
- 着重關注代碼生成以及依賴的三方包。
場景五 java.lang.OutOfMemoryError: Direct buffer memory
/** * VM Args:-XX:MaxDirectMemorySize=4m */ public class DirectMemoryOOM { private static final int _5MB = 5 * 1024 * 1024; public static void main(String[] args) throws Exception { //-XX:MaxDirectMemorySize=4m 本地內存配置的是4MB,這里實際使用的是5MB ByteBuffer.allocateDirect(_5MB); } }
代碼很簡單,分配一個 5M 的直接字節緩沖區。
若在運行程序時指定直接內存的容量大小 -XX:MaxDirectMemorySize 為 4M,則程序運行會出現以下效果:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at DirectMemoryOOM.main(DirectMemoryOOM.java:11)
解決方案:
- 可以通過參數 -XX:MaxDirectMemorySize 適度調整直接內存的容量大小;
- 考慮代碼是否有優化空間。
場景六 java.lang.StackOverflowError
/** * 棧溢出模擬 */ public class StackOOM { public static void main(String[] args) { love1024(); } public static void love1024() { // 遞歸調用 love1024(); } }
代碼很簡單,模擬了一下方法遞歸調用,程序運行效果如下:
Exception in thread "main" java.lang.StackOverflowError at StackOOM.love1024(StackOOM.java:12) at StackOOM.love1024(StackOOM.java:12)
解決方案:
- StackOverflowError 屬於比較好排查的一種錯誤,有錯誤棧可以閱讀,大部分出現這種錯誤,都是程序出現了遞歸調用的問題;
- 如果真需要遞歸調用的存在,可以適度調整參數 -Xss 的大小來解決。
場景七 java.lang.OutOfMemoryError: Unable to create new native thread
/** * 無法創建本地線程模擬 */ public class ThreadUnableCreateOOM { public static void main(String[] args) { while(true) { new Thread(){ @Override public void run() { System.out.println("1024 節日快樂"); try { Thread.sleep(10000); } catch (InterruptedException e) { } } }.start(); } } }
代碼很簡單,模擬了一下業務研發中若一直啟動新的線程去執行任務而帶來的效果,運行如下:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at ThreadUnableCreateOOM.main(ThreadUnableCreateOOM.java:18)
為什么呢?因為當 JVM 向操作系統請求創建一個新線程時,然而操作系統也無法創建新的 native 線程時就會拋出 Unable to create new native thread 錯誤。
解決方案:
- 優化代碼,考慮使用線程池及線程池的數量設置是否合適;
- 檢查操作系統本身的線程數是否可以適度調整。
場景八 java.lang.OutOfMemoryError:Requested array size exceeds VM limit
/** * OutOfMemoryError: Requested array size exceeds VM limit */ public class ArrayLimitOOM { public static void main(String[] args) { int[] ary = new int[Integer.MAX_VALUE]; } }
代碼很簡單,創建一個大小為 Integer.MAX_VALUE 的 int 數組,代碼看起來沒毛病,程序運行起來很詫異:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at ArrayLimitOOM.main(ArrayLimitOOM.java:3)
為什么?當你編寫的 Java 程序試圖要分配大於 Java 虛擬機可以支持的數組時就會報 OOM,Java 對應用程序可以分配的最大數組大小有限制,不同平台限制有所不同。
解決方案:檢查代碼是否有必要創建這么大號的數組,是否可以采用集合、拆分等其它方式處理。