千萬不要這樣寫代碼!9種常見的OOM場景演示!

2021-03-02 macrozheng

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

本篇主要包括如下 OOM 的介紹和示例:

java.lang.StackOverflowErrorjava.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: GC overhead limit exceededjava.lang.OutOfMemoryError-->Metaspacejava.lang.OutOfMemoryError: Direct buffer memoryjava.lang.OutOfMemoryError: unable to create new native threadjava.lang.OutOfMemoryError:Metaspacejava.lang.OutOfMemoryError: Requested array size exceeds VM limitjava.lang.OutOfMemoryError: Out of swap spacejava.lang.OutOfMemoryError:Kill process or sacrifice child

我們常說的 OOM 異常,其實是 Error

一. StackOverflowError1.1 寫個 bug
public class StackOverflowErrorDemo {

    public static void main(String[] args) {
        javaKeeper();
    }

    private static void javaKeeper() {
        javaKeeper();
    }
}

上一篇詳細的介紹過JVM 運行時數據區,JVM 虛擬機棧是有深度的,在執行方法的時候會伴隨著入棧和出棧,上邊的方法可以看到,main 方法執行後不停的遞歸,遲早把棧撐爆了

Exception in thread "main" java.lang.StackOverflowError
 at oom.StackOverflowErrorDemo.javaKeeper(StackOverflowErrorDemo.java:15)

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
/**
 * 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 堆內存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:

如果是超大對象,可以檢查其合理性,比如是否一次性查詢了資料庫全部結果,而沒有做結果數限制如果是業務峰值壓力,可以考慮添加機器資源,或者做限流降級。如果是內存洩漏,需要找到持有的對象,修改代碼設計,比如關閉沒有釋放的連接

面試官:說說內存洩露和內存溢出

加送個知識點,三連的終將成為大神~~

內存洩露和內存溢出

內存溢出(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 個元素。

/**
 * 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 都清理不掉。

🧑🏻‍💻 面試官又來了:說一下HashMap原理以及為什麼需要同時實現equals和hashcode

執行這個程序的最終錯誤,和 JVM 配置也會有關係,如果設置的堆內存特別小,會直接報 Java heap space。算是被這個錯誤截胡了,所以有時,在資源受限的情況下,無法準確預測程序會死於哪種具體的原因。

3.2 解決方案添加 JVM 參數-XX:-UseGCOverheadLimit 不推薦這麼幹,沒有真正解決問題,只是將異常推遲檢查項目中是否有大量的死循環或有使用大內存的代碼,優化代碼dump內存分析,檢查是否存在內存洩露,如果沒有,加大內存四、Direct buffer memory

我們使用 NIO 的時候經常需要使用 ByteBuffer 來讀取或寫入數據,這是一種基於 Channel(通道) 和 Buffer(緩衝區)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆裡面的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣在一些場景就避免了 Java 堆和 Native 中來回複製數據,所以性能會有所提高。

Java 允許應用程式通過 Direct ByteBuffer 直接訪問堆外內存,許多高性能程序通過 Direct ByteBuffer 結合內存映射文件(Memory Mapped File)實現高速 IO。

4.1 寫個 bug

ByteBuffer.allocate(capability) 是分配 JVM 堆內存,屬於 GC 管轄範圍,需要內存拷貝所以速度相對較慢;

ByteBuffer.allocateDirect(capability) 是分配 OS 本地內存,不屬於 GC 管轄範圍,由於不需要內存拷貝所以速度相對較快;

如果不斷分配本地內存,堆內存很少使用,那麼 JVM 就不需要執行 GC,DirectByteBuffer 對象就不會被回收,這時雖然堆內存充足,但本地內存可能已經不夠用了,就會出現 OOM,本地直接內存溢出

/**
 *  VM Options:-Xms10m,-Xmx10m,-XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 */
public class DirectBufferMemoryDemo {

    public static void main(String[] args) {
        System.out.println("maxDirectMemory is:"+sun.misc.VM.maxDirectMemory() / 1024 / 1024 + "MB");

        //ByteBuffer buffer = ByteBuffer.allocate(6*1024*1024);
        ByteBuffer buffer = ByteBuffer.allocateDirect(6*1024*1024);

    }
}

最大直接內存,默認是電腦內存的 1/4,所以我們設小點,然後使用直接內存超過這個值,就會出現 OOM。

maxDirectMemory is:5MB
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

4.2 解決方案Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查檢查是否直接或間接使用了 NIO,如 netty,jetty 等通過啟動參數 -XX:MaxDirectMemorySize 調整 Direct ByteBuffer 的上限值檢查 JVM 參數是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數會使 System.gc() 失效檢查堆外內存使用代碼,確認是否存在內存洩漏;或者通過反射調用 sun.misc.Cleaner 的 clean() 方法來主動釋放被 Direct ByteBuffer 持有的內存空間五、Unable to create new native thread

每個 Java 線程都需要佔用一定的內存空間,當 JVM 向底層作業系統請求創建一個新的 native 線程時,如果沒有足夠的資源分配就會報此類錯誤。

5.1 寫個 bug
public static void main(String[] args) {
  while(true){
    new Thread(() -> {
      try {
        Thread.sleep(Integer.MAX_VALUE);
      } catch(InterruptedException e) { }
    }).start();
  }
}

Error occurred during initialization of VM
java.lang.OutOfMemoryError: unable to create new native thread

5.2 原因分析

JVM 向 OS 請求創建 native 線程失敗,就會拋出 Unableto createnewnativethread,常見的原因包括以下幾類:

線程數超過 kernel.pid_max(只能重啟)native 內存不足;該問題發生的常見過程主要包括以下幾步:JVM 內部的應用程式請求創建一個新的 Java 線程;JVM native 方法代理了該次請求,並向作業系統請求創建一個 native 線程;作業系統嘗試創建一個新的 native 線程,並為其分配內存;如果作業系統的虛擬內存已耗盡,或是受到 32 位進程的地址空間限制,作業系統就會拒絕本次 native 內存分配;JVM 將拋出 java.lang.OutOfMemoryError:Unableto createnewnativethread 錯誤。5.3 解決方案想辦法降低程序中創建線程的數量,分析應用是否真的需要創建這麼多線程如果確實需要創建很多線程,調高 OS 層面的線程最大數:執行 ulimia-a 查看最大線程數限制,使用 ulimit-u xxx 調整最大線程數限制六、Metaspace

JDK 1.8 之前會出現 Permgen space,該錯誤表示永久代(Permanent Generation)已用滿,通常是因為加載的 class 數目太多或體積太大。隨著 1.8 中永久代的取消,就不會出現這種異常了。

Metaspace 是方法區在 HotSpot 中的實現,它與永久代最大的區別在於,元空間並不在虛擬機內存中而是使用本地內存,但是本地內存也有打滿的時候,所以也會有異常。

6.1 寫個 bug
/**
 * JVM Options: -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class MetaspaceOOMDemo {

    public static void main(String[] args) {

        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaspaceOOMDemo.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
                //動態代理創建對象
                return methodProxy.invokeSuper(o, objects);
            });
            enhancer.create();
        }
    }
}

藉助 Spring 的 GCLib 實現動態創建對象

Exception in thread "main" org.springframework.cglib.core.CodeGenerationException: java.lang.OutOfMemoryError-->Metaspace

6.2 解決方案

方法區溢出也是一種常見的內存溢出異常,在經常運行時生成大量動態類的應用場景中,就應該特別關注這些類的回收情況。這類場景除了上邊的 GCLib 字節碼增強和動態語言外,常見的還有,大量 JSP 或動態產生 JSP  文件的應用(遠古時代的傳統軟體行業可能會有)、基於 OSGi 的應用(即使同一個類文件,被不同的加載器加載也會視為不同的類)等。

方法區在 JDK8 中一般不太容易產生,HotSpot 提供了一些參數來設置元空間,可以起到預防作用

-XX:MaxMetaspaceSize 設置元空間最大值,默認是 -1,表示不限制(還是要受本地內存大小限制的)-XX:MetaspaceSize 指定元空間的初始空間大小,以字節為單位,達到該值就會觸發 GC 進行類型卸載,同時收集器會對該值進行調整-XX:MinMetaspaceFreeRatio 在 GC 之後控制最小的元空間剩餘容量的百分比,可減少因元空間不足導致的垃圾收集頻率,類似的還有 MaxMetaspaceFreeRatio七、Requested array size exceeds VM limit7.1 寫個 bug
public static void main(String[] args) {
  int[] arr = new int[Integer.MAX_VALUE];
}

這個比較簡單,建個超級大數組就會出現 OOM,不多說了

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit

JVM 限制了數組的最大長度,該錯誤表示程序請求創建的數組超過最大長度限制。

JVM 在為數組分配內存前,會檢查要分配的數據結構在系統中是否可尋址,通常為 Integer.MAX_VALUE-2。

此類問題比較罕見,通常需要檢查代碼,確認業務是否需要創建如此大的數組,是否可以拆分為多個塊,分批執行。

八、Out of swap space

啟動 Java 應用程式會分配有限的內存。此限制是通過-Xmx和其他類似的啟動參數指定的。

在 JVM 請求的總內存大於可用物理內存的情況下,作業系統開始將內容從內存換出到硬碟驅動器。

該錯誤表示所有可用的虛擬內存已被耗盡。虛擬內存(Virtual Memory)由物理內存(Physical Memory)和交換空間(Swap Space)兩部分組成。

這種錯誤沒見過~~~

九、Kill process or sacrifice child

作業系統是建立在流程概念之上的。這些進程由幾個內核作業負責,其中一個名為「 Out of memory Killer」,它會在可用內存極低的情況下「殺死」(kill)某些進程。OOM Killer 會對所有進程進行打分,然後將評分較低的進程「殺死」,具體的評分規則可以參考 Surviving the Linux OOM Killer。

不同於其他的 OOM 錯誤, Killprocessorsacrifice child 錯誤不是由 JVM 層面觸發的,而是由作業系統層面觸發的。

9.1 原因分析

默認情況下,Linux 內核允許進程申請的內存總量大於系統可用內存,通過這種「錯峰復用」的方式可以更有效的利用系統資源。

然而,這種方式也會無可避免地帶來一定的「超賣」風險。例如某些進程持續佔用系統內存,然後導致其他進程沒有可用內存。此時,系統將自動激活 OOM Killer,尋找評分低的進程,並將其「殺死」,釋放內存資源。

9.2 解決方案

最後附上一張「涯海」大神的圖

涯海參考與感謝

《深入理解 Java 虛擬機 第 3 版》

https://plumbr.io/outofmemoryerror

https://yq.aliyun.com/articles/711191

https://github.com/StabilityMan/StabilityGuide/blob/master/docs/diagnosis/jvm/exception

歡迎關注,點個在看

相關焦點

  • 教你寫Bug,常見的 OOM 異常分析
    1.2 原因分析無限遞歸循環調用(最常見原因),要時刻注意代碼中是否有了循環調用方法而無法退出的情況native 代碼有棧上分配的邏輯,並且要求的內存還不小,比如 java.net.SocketInputStream.read0 會在棧上要求分配一個
  • 郭健:Linux內存管理系統參數配置之OOM(內存耗盡)
    如果這個名詞對你毫無壓力,你可以直接進入第三章,這一章是描述具體的參數的,除了描述具體的參數,我們引用了一些具體的內核代碼,本文的代碼來自4.0內核,如果有興趣,可以結合代碼閱讀,為了縮減篇幅,文章中的代碼都是刪減版本的。按照慣例,最後一章是參考文獻,本文的參考文獻都是來自linux內核的Documentation目錄,該目錄下有大量的文檔可以參考,每一篇都值得細細品味。
  • 新手寫代碼常見的錯誤示例
    新手寫代碼常見的錯誤示例 每個程式設計師都要經歷寫代碼從不規範到規範的過程,下面總結了一些新手寫代碼常見的錯誤示例,看看有沒有你吧~ 1、命名不規範 命名很隨意
  • 高手總結的9種 OOM 常見原因及解決方案
    本文總結了常見的 OOM 原因及其解決方法,如下圖所示。如有遺漏或錯誤,歡迎補充指正。原因分析Javaheap space 錯誤產生的常見原因可以分為以下幾類:1、請求創建一個超大對象,通常是一個大數組。2、超出預期的訪問量/數據量,通常是上遊系統請求流量飆升,常見於各類促銷/秒殺活動,可以結合業務流量指標排查是否有尖狀峰值。3、過度使用終結器(Finalizer),該對象沒有立即被 GC。
  • Kubernetes 服務質量和 OOM 詳解
    BestEffort註:資源只是 Pod 是否能運行的一個檢查項,QoS 類為 BestEffort 的 Pod 並不是始終可調度的。,如上所示,CPU 資源調用和限制都是非零的,因為我們沒有對它做任何設置(設置就成了 Burstable)。但這裡我們要關注的並不是 CPU 資源被用了多少,而是在「unlimited CPU」的情況下,Pod 在使用 CPU 時幾乎沒有得到任何優先級。
  • 面試官:哪些場景會產生OOM?怎麼解決?
    初看好像挺簡單的,其實深究起來考察的是對整個JVM的了解,而且這個問題從網上可以翻到一些亂七八糟的答案,其實在總結下來基本上4個場景可以概括下來。堆內存溢出堆內存溢出太常見,大部分人都應該能想得到這一點,堆內存用來存儲對象實例,我們只要不停的創建對象,並且保證GC Roots和對象之間有可達路徑避免垃圾回收,那麼在對象數量超過最大堆的大小限制後很快就能出現這個異常。寫一段代碼測試一下,設置堆內存大小2M。
  • 代碼這樣寫更優雅(Python版)
    點擊文章末尾閱讀原文報名作者微課
  • 如何優雅地在夏威夷海灘上寫代碼
    其實真正的標題應該是《如何優雅地直播在夏威夷海灘上寫代碼》或《如何優雅地在夏威夷海灘上直播寫代碼》。  前幾天,Anthony跟我聊,說他找到一個開源的庫,可以做Chromakey,問我能不能用它來做點好玩的事情。我還是第一次聽說Chromakey,抓緊查了查,懂了,不就是個虛擬演播室麼。以前,參加各種新科技展會的時候曾玩過幾次。
  • Flash中常見的代碼錯誤
    在Flash代碼編寫時,會遇到各種各樣的錯誤,在這裡就說明一些常見錯誤的原因和解決方法。
  • 用long類型讓我出了次生產事故,寫代碼還是要小心點
    昨天發現線上試跑期的一個程序掛了,平時都跑的好好的,查了下日誌是因為昨天運營跑了一家美妝top級淘品牌店,會員量近千萬,一下子就把128G的內存給爆了,當時並行跑了二個任務,沒轍先速寫一段代碼限流,後面再做進一步優化。一: 背景1.
  • 初中生「最常見」的4種字體,閱卷老師不喜歡,中考千萬別這樣寫
    初中生的卷子最不好判卷,因為有很多同學的字實在是慘不忍睹,今天就帶大家盤點一下,初中生最常見的4種字體,同時也被閱卷老師「瘋狂吐槽」,卷面分幾乎為0的字體。每次看到這樣的字閱卷老師也是很心累,雖然看的清楚,但不自覺的就會把頭向一側偏去,在批這樣的卷子時,很容易感到疲憊。
  • 初中生「最常見」的4種字體,閱卷老師不喜歡,中考千萬別這樣寫
    初中生的卷子最不好判卷,因為有很多同學的字實在是慘不忍睹,今天就帶大家盤點一下,初中生最常見的4種字體,同時也被閱卷老師「瘋狂吐槽」,卷面分幾乎為0的字體。一.橫不平,豎不直的「狂草」初中老師說過,答題的時候最好是用正楷,這樣的字不僅橫平豎直,還工整好看。
  • 別再用Else語句寫代碼了!
    作者丨Joey Colon 譯者丨核子可樂 策劃丨小智 if…else 語句是許多程式設計師在寫代碼時最常用的方式之一。你甚至可以看到許多程式設計師的代碼中嵌套著無數 else 語句。可這樣,真的好嗎?
  • 常見的幾種賣淨水器的套路,千萬不要被忽悠了!
    那這樣說來我們每個人天天抬著杯水喝就得啦,既沒有生老病死,也沒有相思成疾!這樣的商家居然也能銷售很多淨水器。先不說鄰居是他們從哪找到的,但水是我們每天必須喝進肚子的東西,這種劣質淨水器拿來賣,真不知道良心能不能過得去!
  • 遊民晨播報:《噬血代碼》超長演示 《絕地求生:大逃殺》銷量突破...
    IGN公布了萬代南夢宮旗下的動作遊戲《噬血代碼》的一段17分鐘的超長演示,《絕地求生:大逃殺》銷量突破1000萬份。還有更多內容,我們一起來看看吧。《噬血代碼》新演示和新情報  IGN公布了萬代南夢宮旗下的動作遊戲《噬血代碼》的一段17分鐘的超長演示,展示了遊戲中的戰鬥。大家可以從中感受一下遊戲的風格。而新的一期Fami通雜誌上,公布了遊戲新的角色和遊戲系統的情報。其中兩名新角色叫做Rui和Io。在遊戲中玩家可以自定義角色外觀。更多內容請關注我們稍後帶來的詳細報導。
  • 十大經典排序算法(動態演示+代碼)!乾貨收藏
    冒泡排序動圖演示代碼希爾排序動圖演示代碼代碼:include <vector>34; "; cout << endl; return 0;}9 桶排序桶排序動圖演示代碼
  • 程式設計師們,請你們不要排斥零代碼
    當然,可愛的程式設計師們往往都很含蓄,他們不會說——「零代碼平臺有什麼鳥用?如果不寫代碼就能夠開發軟體,還要我們幹嗎?」。他們不會說,但我們心裡知道。當我們向一位潛在顧客演示明道雲後,我幾乎都能聽到有幾位程式設計師心裡的想法。有時候,客戶不同職能的人圍繞是否要採納零代碼系統當面爭執起來,我也有點尷尬。
  • [精選] 寫代碼有這10個好習慣的話,可以減少80%非業務的bug
    修改完代碼,記得自己測試一下「改完代碼,自己先測試」是每位程序猿們必備的基本素養。尤其不要抱著這種「僥倖心理:我只是改了一個變量或者我只改了一行配置代碼,沒必要自己測試」。改完代碼後最好要求自己去測試一下,這樣可以規避很多不必要的bug。