教你寫Bug,常見的 OOM 異常分析

2021-02-20 不加班程式設計師


在《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

相關焦點

  • 千萬不要這樣寫代碼!9種常見的OOM場景演示!
    StackOverflowError1.1 寫個 bugpublic class StackOverflowErrorDemo {    public static void main(String[] args) {        javaKeeper();    }    private static
  • 教你學習:Python-100-Days-11 文件與異常
    教你學習: Python-100-Days-11 文件與異常本項目是參考項目內容,進行個人理解,和原有項目有出入,如想了解詳情,請自行百度去搜索項目文件的讀取是我們平時經常遇到的事,我們打開電腦往txt裡沒寫今天的日記,改天打開文件查看我們之前記錄的,這個過程就是文件的讀寫操作。
  • Kubernetes 服務質量和 OOM 詳解
    BestEffort註:資源只是 Pod 是否能運行的一個檢查項,QoS 類為 BestEffort 的 Pod 並不是始終可調度的。,如上所示,CPU 資源調用和限制都是非零的,因為我們沒有對它做任何設置(設置就成了 Burstable)。但這裡我們要關注的並不是 CPU 資源被用了多少,而是在「unlimited CPU」的情況下,Pod 在使用 CPU 時幾乎沒有得到任何優先級。
  • cuisine royale遊戲bug有哪些?常見bug解決攻略
    cuisine royale最常見的BUG有哪些?這款遊戲也是目前異常火爆的吃雞手遊,吃雞遊戲多多少少都會出現一些BUG,那麼遇到這些BUG有沒有什麼解決辦法?下面我們來盤點一下常見的幾個BUG以及解決辦 cuisine royale最常見的BUG有哪些?
  • plink分析遇到的bug
    pca分析命令,這裡的例子是結合 GCTA 的GRM進行的分析,代碼如下,vcftools --gzvcf test.vcf.gz --plink --out testplink --file test --chr-set 23 --make-bed --out testgcta64 --bfile test --make-grm
  • 我寫了個bug,谷歌面試官卻說:你通過了...
    一邊有人反饋面試bar拔高,算法偏難、加試普遍;也有人卻說寫了個bug,依然過了面試...最頂尖的工程師也很難在面試中一次性做到bug free,所以通常面試官不會因為代碼出現bug就掛掉你的面試。他們往往更注重考查:
  • 一個神奇的bug:OOM?優雅終止線程?系統內存佔用較高?
    其實並非如此,感興趣的同學可以把循環次數加的更大一些,在循環開始幾次就進行interrupt,你會發現結果還是這樣。; }}控制臺輸出測試結果分析這個測試驗證了前面關於線程異常終止的結論:線程執行中拋出Throwable對象且不被顯式捕獲,JVM會終止線程。
  • 郭健:Linux內存管理系統參數配置之OOM(內存耗盡)
    如果這個名詞對你毫無壓力,你可以直接進入第三章,這一章是描述具體的參數的,除了描述具體的參數,我們引用了一些具體的內核代碼,本文的代碼來自4.0內核,如果有興趣,可以結合代碼閱讀,為了縮減篇幅,文章中的代碼都是刪減版本的。按照慣例,最後一章是參考文獻,本文的參考文獻都是來自linux內核的Documentation目錄,該目錄下有大量的文檔可以參考,每一篇都值得細細品味。
  • VMware虛擬機怎麼分析bug記錄?
    VMware虛擬機怎麼分析bug記錄?虛擬機會有一些莫名其妙的問題,好在可以在Windows7正常系統中驗證問題的範圍,外圍正常,就是虛擬機內部機制的問題。6、但是在Windows7作業系統中輸入whd完全沒問題,所以虛擬機的bug定位!(比較奇怪的問題)7、本來懷疑是搜狗拼音輸入法,卻發現不使用拼音輸入法,輸入:whd也會自動最小化。8、關閉、重啟虛擬機,仍然異常如故!bug確定無誤。9、輸入:hwd 三個字母之後,程序自動最小化,這種bug聞所未聞!
  • Java常見異常及解釋
    常見 Java 異常解釋:(譯者註:非技術角度分析。
  • 從 OOM 到 iOS 內存詳解 - 上 OOM
    感謝你閱讀本文,如果你喜歡的話,歡迎關注「 iOS 成長指北 」,如果本文對你有所幫助
  • 數據指標出現異常波動時,你該如何進行異常分析呢?
    當APP產品業務線的某個數據指標出現異常的波動時,該如何著手數據異常分析呢?這也是面試數據方面的工作比較常見的問題。那麼,今天將系統的梳理總結一下這類問題的分析框架以及需要考慮的問題,今後在遇到此類問題時,希望能有一個明確的著力點以及分析思維。
  • [精選] 寫代碼有這10個好習慣的話,可以減少80%非業務的bug
    這個儘量養成習慣吧,要知道很多「低級bug」都是「不校驗參數」導致的。❝比如你的資料庫欄位設置為varchar(16),對方傳了一個32位的字符串過來,你不校驗參數,「插入資料庫那就直接異常」了。這個經常發現沒?❞3.
  • 數據異常如何分析?
    一、數據異動分析方法論面對數據異常通常有五步:(1)發現異常就像你發現昨天數據跟往前不一樣,猛漲了還是猛跌了,通過觀測數據發現異常。二、案例解析舉個慄子:你近期入職了某網際網路公司,公司的業務方向是做陌生人社交的,在處理數據的過程中,你發現在你入職的前一天,數據是異常的,特別想分析出原因是什麼?那如何操作?
  • 做不到Bug Free,面試是不是就涼了?Coding面試有了Bug如何緊急自救?
    今天小編就來說說如果出現了bug,怎麼力挽狂瀾把在面試官心目中的形象分扳回來。 首先大家要明確的是,並不是寫出了code,做到了基本的bug free,就代表你coding能力很solid。大家可以根據以下十二條自查一下,檢測自己寫code的能力是否solid: 1)在寫code之前,有沒有主動的跟面試官溝通來明確題目要求,分析各種需要處理的情形?
  • 典型案例:Bug 9776608-多個用戶使用錯誤密碼登錄同一個用戶而造成的用戶無法登錄異常
    墨墨導讀:在Oracle 11g中,大量的登錄失敗可能會導致library cache lock;或者大量的使用同一用戶登錄且登錄失敗,導致用戶登錄hang的問題,本文記錄整個分析二、問題處理過程及分析方法通過遠程,sqlplus / as sysdba對資料庫進行登錄,並進行檢查,資料庫運行正常,且資料庫中沒有異常的等待事件;根據客戶描述,通過wx用戶和客戶提供的密碼進行登錄,發現登錄出現hang住的情況,重新打開另外一個資料庫窗口,並對當前的阻塞進行排查:
  • mPaaS框架下如何使用 CrashSDK 對閃退進行分析?
    閃退報文分析工具介紹對於 mPaaS 的用戶,從 MAS 上閃退分析平臺導出的一般是原始的閃退信息,閃退信息比較多,如果直接閱讀會比較困難,使用者可以通過下載 Chrome 的插件 LogAnalyzer。
  • 一篇文章了解python常見內置異常報錯
    我們在寫python程序的時候經常會遇到一些報錯信息(異常),有一些可能是人為進行的定義
  • Power BI異常檢測器教你玩
    ▲時間序列數據是同一指標按時間順序記錄的數據列分析時間序列數據通常從兩個方面出發,一個方面是預測,另一個方面就是異常檢測(Anomaly detection)。 以商業分析為例,你是一個餐飲集團的老闆,你最關心的數據是每天不同店鋪的營業額。你想要時刻檢查這些數據,你想要實時監控這些數據,你想要知道這些數據什麼時候會出現異常,你想要知道這些數據出現異常的原因,這樣你才能在造成更大的損失前來及時止損。
  • Appium自身非必現bug影響自動化測試正常運行?Try-Catch幫您解決
    在進行軟體UI自動化測試的時候,經常會出現非被測軟體bug導致的測試執行失敗,常見原因包括:測試腳本的問題、網絡問題、產品UI的變更等等。還有一種情況是測試工具自身的bug,特別是偶現的bug並且開源的自動化測試工具還沒有修復該bug的時候,我們關心的是如何繞過這個已知bug繼續使用appium進行其他場景的自動化測試,本文通過一個實例進行分析,分兩步闡述一、第一步-----------使用appium測試抖音軟體目標