1. 程式人生 > >10種常見OOM分析——手把手教你寫bug

10種常見OOM分析——手把手教你寫bug

> 點贊+收藏 就學會系列,文章收錄在 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper) ,N線網際網路開發必備技能兵器譜,筆記自取 ![](https://tva1.sinaimg.cn/large/007S8ZIlly1gggcqh5gsdj324d0ol0y7.jpg) 在《Java虛擬機器規範》的規定裡,除了程式計數器外,虛擬機器記憶體的其他幾個執行時區域都有發生 OutOfMemoryError 異常的可能。 本篇主要包括如下 OOM 的介紹和示例: - java.lang.StackOverflowError - java.lang.OutOfMemoryError: Java heap space - java.lang.OutOfMemoryError: GC overhead limit exceeded - java.lang.OutOfMemoryError-->Metaspace - java.lang.OutOfMemoryError: Direct buffer memory - java.lang.OutOfMemoryError: unable to create new native thread - java.lang.OutOfMemoryError:Metaspace - java.lang.OutOfMemoryError: Requested array size exceeds VM limit - java.lang.OutOfMemoryError: Out of swap space - java.lang.OutOfMemoryError:Kill process or sacrifice child > 我們常說的 OOM 異常,其實是 Error ![](https://tva1.sinaimg.cn/large/007S8ZIlly1gggbu55wwgj30sy0ku3z0.jpg) ## 一. StackOverflowError ### 1.1 寫個 bug ```java public class StackOverflowErrorDemo { public static void main(String[] args) { javaKeeper(); } private static void javaKeeper() { javaKeeper(); } } ``` 上一篇詳細的介紹過[JVM 執行時資料區](https://mp.weixin.qq.com/s/jPIHNsQwiYNCRUQt1qXR6Q),JVM 虛擬機器棧是有深度的,在執行方法的時候會伴隨著入棧和出棧,上邊的方法可以看到,main 方法執行後不停的遞迴,遲早把棧撐爆了 ``` Exception in thread "main" java.lang.StackOverflowError at oom.StackOverflowErrorDemo.javaKeeper(StackOverflowErrorDemo.java:15) ``` ![](https://i02piccdn.sogoucdn.com/b334c2faa77a5e03) ### 1.2 原因分析 - 無限遞迴迴圈呼叫(最常見原因),要時刻注意程式碼中是否有了迴圈呼叫方法而無法退出的情況 - 執行了大量方法,導致執行緒棧空間耗盡 - 方法內聲明瞭海量的區域性變數 - native 程式碼有棧上分配的邏輯,並且要求的記憶體還不小,比如 java.net.SocketInputStream.read0 會在棧上要求分配一個 64KB 的快取(64位 Linux) ### 1.3 解決方案 - 修復引發無限遞迴呼叫的異常程式碼, 通過程式丟擲的異常堆疊,找出不斷重複的程式碼行,按圖索驥,修復無限遞迴 Bug - 排查是否存在類之間的迴圈依賴(當兩個物件相互引用,在呼叫toString方法時也會產生這個異常) - 通過 JVM 啟動引數 `-Xss` 增加執行緒棧記憶體空間, 某些正常使用場景需要執行大量方法或包含大量區域性變數,這時可以適當地提高執行緒棧空間限制 ## 二. Java heap space Java 堆用於儲存物件例項,我們只要不斷的建立物件,並且保證 GC Roots 到物件之間有可達路徑來避免 GC 清除這些物件,那隨著物件數量的增加,總容量觸及堆的最大容量限制後就會產生記憶體溢位異常。 Java 堆記憶體的 OOM 異常是實際應用中最常見的記憶體溢位異常。 ### 2.1 寫個 bug ```java /** * JVM引數:-Xmx12m */ public class JavaHeapSpaceDemo { static final int SIZE = 2 * 1024 * 1024; public static void main(String[] a) { int[] i = new int[SIZE]; } } ``` 程式碼試圖分配容量為 2M 的 int 陣列,如果指定啟動引數 `-Xmx12m`,分配記憶體就不夠用,就類似於將 XXXL 號的物件,往 S 號的 Java heap space 裡面塞。 ``` Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at oom.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:13) ``` ### 2.2 原因分析 - 請求建立一個超大物件,通常是一個大陣列 - 超出預期的訪問量/資料量,通常是上游系統請求流量飆升,常見於各類促銷/秒殺活動,可以結合業務流量指標排查是否有尖狀峰值 - 過度使用終結器(Finalizer),該物件沒有立即被 GC - 記憶體洩漏(Memory Leak),大量物件引用沒有釋放,JVM 無法對其自動回收,常見於使用了 File 等資源沒有回收 ### 2.3 解決方案 針對大部分情況,通常只需要通過 `-Xmx` 引數調高 JVM 堆記憶體空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理: - 如果是超大物件,可以檢查其合理性,比如是否一次性查詢了資料庫全部結果,而沒有做結果數限制 - 如果是業務峰值壓力,可以考慮新增機器資源,或者做限流降級。 - 如果是記憶體洩漏,需要找到持有的物件,修改程式碼設計,比如關閉沒有釋放的連線 ![img](https://i03piccdn.sogoucdn.com/1b2bed506484c61d) > 面試官:說說記憶體洩露和記憶體溢位 加送個知識點,三連的終將成為大神~~ ## 記憶體洩露和記憶體溢位 記憶體溢位(out of memory),是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個 Integer,但給它存了 Long 才能存下的數,那就是記憶體溢位。 記憶體洩露( memory leak),是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。 **memory leak 最終會導致 out of memory!** ## 三、GC overhead limit exceeded JVM 內建了垃圾回收機制GC,所以作為 Javaer 的我們不需要手工編寫程式碼來進行記憶體分配和釋放,但是當 Java 程序花費 98% 以上的時間執行 GC,但只恢復了不到 2% 的記憶體,且該動作連續重複了 5 次,就會丟擲 `java.lang.OutOfMemoryError:GC overhead limit exceeded` 錯誤(**俗稱:垃圾回收上頭**)。簡單地說,就是應用程式已經基本耗盡了所有可用記憶體, GC 也無法回收。 假如不丟擲 `GC overhead limit exceeded` 錯誤,那 GC 清理的那麼一丟丟記憶體很快就會被再次填滿,迫使 GC 再次執行,這樣惡性迴圈,CPU 使用率 100%,而 GC 沒什麼效果。 ### 3.1 寫個 bug 出現這個錯誤的例項,其實我們寫個無限迴圈,往 List 或 Map 加資料就會一直 Full GC,直到扛不住,這裡用一個不容易發現的栗子。我們往 map 中新增 1000 個元素。 ```java /** * JVM 引數: -Xmx14m -XX:+PrintGCDetails */ public class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map m = new HashMap(); while (true){ for (int i = 0; i < 1000; i++){ if (!m.containsKey(new Key(i))){ m.put(new Key(i), "Number:" + i); } } System.out.println("m.size()=" + m.size()); } } } ``` ``` ... m.size()=54000 m.size()=55000 m.size()=56000 Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded ``` 從輸出結果可以看到,我們的限制 1000 條資料沒有起作用,map 容量遠超過了 1000,而且最後也出現了我們想要的錯誤,這是因為類 Key 只重寫了 `hashCode()` 方法,卻沒有重寫 `equals()` 方法,我們在使用 `containsKey()` 方法其實就出現了問題,於是就會一直往 HashMap 中新增 Key,直至 GC 都清理不掉。 >