<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 6822字,需瀏覽 14分鐘

           ·

          2020-07-21 20:23

          d0139a97bed40601fa066c19c1a387c6.webp

          在《Java虛擬機規(guī)范》的規(guī)定里,除了程序計數(shù)器外,虛擬機內(nèi)存的其他幾個運行時區(qū)域都有發(fā)生 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

          17ba9599df69b884a58de08888bee8f5.webp

          一. StackOverflowError

          1.1 寫個 bug

          public?class?StackOverflowErrorDemo?{

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

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

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

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

          1.2 原因分析

          • 無限遞歸循環(huán)調(diào)用(最常見原因),要時刻注意代碼中是否有了循環(huán)調(diào)用方法而無法退出的情況
          • 執(zhí)行了大量方法,導(dǎo)致線程棧空間耗盡
          • 方法內(nèi)聲明了海量的局部變量
          • native 代碼有棧上分配的邏輯,并且要求的內(nèi)存還不小,比如 java.net.SocketInputStream.read0 會在棧上要求分配一個 64KB 的緩存(64位 Linux)

          1.3 解決方案

          • 修復(fù)引發(fā)無限遞歸調(diào)用的異常代碼, 通過程序拋出的異常堆棧,找出不斷重復(fù)的代碼行,按圖索驥,修復(fù)無限遞歸 Bug
          • 排查是否存在類之間的循環(huán)依賴(當兩個對象相互引用,在調(diào)用toString方法時也會產(chǎn)生這個異常)
          • 通過 JVM 啟動參數(shù) -Xss 增加線程棧內(nèi)存空間, 某些正常使用場景需要執(zhí)行大量方法或包含大量局部變量,這時可以適當?shù)靥岣呔€程棧空間限制

          二. Java heap space

          Java 堆用于存儲對象實例,我們只要不斷的創(chuàng)建對象,并且保證 GC Roots 到對象之間有可達路徑來避免 GC 清除這些對象,那隨著對象數(shù)量的增加,總?cè)萘坑|及堆的最大容量限制后就會產(chǎn)生內(nèi)存溢出異常。

          Java 堆內(nèi)存的 OOM 異常是實際應(yīng)用中最常見的內(nèi)存溢出異常。

          2.1 寫個 bug

          /**
          ?* JVM參數(shù):-Xmx12m
          ?*/

          public?class?JavaHeapSpaceDemo?{

          ????static?final?int?SIZE?=?2?*?1024?*?1024;

          ????public?static?void?main(String[]?a)?{
          ????????int[]?i?=?new?int[SIZE];
          ????}
          }

          代碼試圖分配容量為 2M 的 int 數(shù)組,如果指定啟動參數(shù) -Xmx12m,分配內(nèi)存就不夠用,就類似于將 XXXL 號的對象,往 S 號的 Java heap space 里面塞。

          Exception?in?thread?"main"?java.lang.OutOfMemoryError:?Java?heap?space
          ?at?oom.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:13)

          2.2 原因分析

          • 請求創(chuàng)建一個超大對象,通常是一個大數(shù)組
          • 超出預(yù)期的訪問量/數(shù)據(jù)量,通常是上游系統(tǒng)請求流量飆升,常見于各類促銷/秒殺活動,可以結(jié)合業(yè)務(wù)流量指標排查是否有尖狀峰值
          • 過度使用終結(jié)器(Finalizer),該對象沒有立即被 GC
          • 內(nèi)存泄漏(Memory Leak),大量對象引用沒有釋放,JVM 無法對其自動回收,常見于使用了 File 等資源沒有回收

          2.3 解決方案

          針對大部分情況,通常只需要通過 -Xmx 參數(shù)調(diào)高 JVM 堆內(nèi)存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:

          • 如果是超大對象,可以檢查其合理性,比如是否一次性查詢了數(shù)據(jù)庫全部結(jié)果,而沒有做結(jié)果數(shù)限制
          • 如果是業(yè)務(wù)峰值壓力,可以考慮添加機器資源,或者做限流降級。
          • 如果是內(nèi)存泄漏,需要找到持有的對象,修改代碼設(shè)計,比如關(guān)閉沒有釋放的連接

          面試官:說說內(nèi)存泄露和內(nèi)存溢出

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

          內(nèi)存泄露和內(nèi)存溢出

          內(nèi)存溢出(out of memory),是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,出現(xiàn)out of memory;比如申請了一個 Integer,但給它存了 Long 才能存下的數(shù),那就是內(nèi)存溢出。

          內(nèi)存泄露( memory leak),是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光。

          memory leak 最終會導(dǎo)致 out of memory!

          三、GC overhead limit exceeded

          JVM 內(nèi)置了垃圾回收機制GC,所以作為 Javaer 的我們不需要手工編寫代碼來進行內(nèi)存分配和釋放,但是當 Java 進程花費 98% 以上的時間執(zhí)行 GC,但只恢復(fù)了不到 2% 的內(nèi)存,且該動作連續(xù)重復(fù)了 5 次,就會拋出 java.lang.OutOfMemoryError:GC overhead limit exceeded 錯誤(俗稱:垃圾回收上頭)。簡單地說,就是應(yīng)用程序已經(jīng)基本耗盡了所有可用內(nèi)存, GC 也無法回收。

          假如不拋出 GC overhead limit exceeded 錯誤,那 GC 清理的那么一丟丟內(nèi)存很快就會被再次填滿,迫使 GC 再次執(zhí)行,這樣惡性循環(huán),CPU 使用率 100%,而 GC 沒什么效果。

          3.1 寫個 bug

          出現(xiàn)這個錯誤的實例,其實我們寫個無限循環(huán),往 List 或 Map 加數(shù)據(jù)就會一直 Full GC,直到扛不住,這里用一個不容易發(fā)現(xiàn)的栗子。我們往 map 中添加 1000 個元素。

          /**
          ?* JVM 參數(shù):?-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

          從輸出結(jié)果可以看到,我們的限制 1000 條數(shù)據(jù)沒有起作用,map 容量遠超過了 1000,而且最后也出現(xiàn)了我們想要的錯誤,這是因為類 Key 只重寫了 hashCode() 方法,卻沒有重寫 equals() 方法,我們在使用 containsKey() 方法其實就出現(xiàn)了問題,于是就會一直往 HashMap 中添加 Key,直至 GC 都清理不掉。

          ???? 面試官又來了:說一下HashMap原理以及為什么需要同時實現(xiàn)equals和hashcode

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

          3.2 解決方案

          • 添加 JVM 參數(shù)-XX:-UseGCOverheadLimit 不推薦這么干,沒有真正解決問題,只是將異常推遲
          • 檢查項目中是否有大量的死循環(huán)或有使用大內(nèi)存的代碼,優(yōu)化代碼
          • dump內(nèi)存分析,檢查是否存在內(nèi)存泄露,如果沒有,加大內(nèi)存

          四、Direct buffer memory

          我們使用 NIO 的時候經(jīng)常需要使用 ByteBuffer 來讀取或?qū)懭霐?shù)據(jù),這是一種基于 Channel(通道) 和 Buffer(緩沖區(qū))的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在 Java 堆里面的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作。這樣在一些場景就避免了 Java 堆和 Native 中來回復(fù)制數(shù)據(jù),所以性能會有所提高。

          Java 允許應(yīng)用程序通過 Direct ByteBuffer 直接訪問堆外內(nèi)存,許多高性能程序通過 Direct ByteBuffer 結(jié)合內(nèi)存映射文件(Memory Mapped File)實現(xiàn)高速 IO。

          4.1 寫個 bug

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

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

          如果不斷分配本地內(nèi)存,堆內(nèi)存很少使用,那么 JVM 就不需要執(zhí)行 GC,DirectByteBuffer 對象就不會被回收,這時雖然堆內(nèi)存充足,但本地內(nèi)存可能已經(jīng)不夠用了,就會出現(xiàn) OOM,本地直接內(nèi)存溢出

          /**
          ?*? 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);

          ????}
          }

          最大直接內(nèi)存,默認是電腦內(nèi)存的 1/4,所以我們設(shè)小點,然后使用直接內(nèi)存超過這個值,就會出現(xiàn) OOM。

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

          4.2 解決方案

          1. Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查
          2. 檢查是否直接或間接使用了 NIO,如 netty,jetty 等
          3. 通過啟動參數(shù) -XX:MaxDirectMemorySize 調(diào)整 Direct ByteBuffer 的上限值
          4. 檢查 JVM 參數(shù)是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數(shù)會使 System.gc() 失效
          5. 檢查堆外內(nèi)存使用代碼,確認是否存在內(nèi)存泄漏;或者通過反射調(diào)用 sun.misc.Cleanerclean() 方法來主動釋放被 Direct ByteBuffer 持有的內(nèi)存空間
          6. 內(nèi)存容量確實不足,升級配置

          五、Unable to create new native thread

          每個 Java 線程都需要占用一定的內(nèi)存空間,當 JVM 向底層操作系統(tǒng)請求創(chuàng)建一個新的 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 原因分析

          91d68c3a80509ad425cb5cb7d236028a.webp

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

          • 線程數(shù)超過操作系統(tǒng)最大線程數(shù)限制(和平臺有關(guān))
          • 線程數(shù)超過 kernel.pid_max(只能重啟)
          • native 內(nèi)存不足;該問題發(fā)生的常見過程主要包括以下幾步:
          1. JVM 內(nèi)部的應(yīng)用程序請求創(chuàng)建一個新的 Java 線程;
          2. JVM native 方法代理了該次請求,并向操作系統(tǒng)請求創(chuàng)建一個 native 線程;
          3. 操作系統(tǒng)嘗試創(chuàng)建一個新的 native 線程,并為其分配內(nèi)存;
          4. 如果操作系統(tǒng)的虛擬內(nèi)存已耗盡,或是受到 32 位進程的地址空間限制,操作系統(tǒng)就會拒絕本次 native 內(nèi)存分配;
          5. JVM 將拋出 java.lang.OutOfMemoryError:Unableto createnewnativethread 錯誤。

          5.3 解決方案

          1. 想辦法降低程序中創(chuàng)建線程的數(shù)量,分析應(yīng)用是否真的需要創(chuàng)建這么多線程
          2. 如果確實需要創(chuàng)建很多線程,調(diào)高 OS 層面的線程最大數(shù):執(zhí)行 ulimia-a 查看最大線程數(shù)限制,使用 ulimit-u xxx 調(diào)整最大線程數(shù)限制

          六、Metaspace

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

          Metaspace 是方法區(qū)在 HotSpot 中的實現(xiàn),它與永久代最大的區(qū)別在于,元空間并不在虛擬機內(nèi)存中而是使用本地內(nèi)存,但是本地內(nèi)存也有打滿的時候,所以也會有異常。

          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)?->?{
          ????????????????//動態(tài)代理創(chuàng)建對象
          ????????????????return?methodProxy.invokeSuper(o,?objects);
          ????????????});
          ????????????enhancer.create();
          ????????}
          ????}
          }

          借助 Spring 的 GCLib 實現(xiàn)動態(tài)創(chuàng)建對象

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

          6.2 解決方案

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

          方法區(qū)在 JDK8 中一般不太容易產(chǎn)生,HotSpot 提供了一些參數(shù)來設(shè)置元空間,可以起到預(yù)防作用

          • -XX:MaxMetaspaceSize 設(shè)置元空間最大值,默認是 -1,表示不限制(還是要受本地內(nèi)存大小限制的)
          • -XX:MetaspaceSize 指定元空間的初始空間大小,以字節(jié)為單位,達到該值就會觸發(fā) GC 進行類型卸載,同時收集器會對該值進行調(diào)整
          • -XX:MinMetaspaceFreeRatio 在 GC 之后控制最小的元空間剩余容量的百分比,可減少因元空間不足導(dǎo)致的垃圾收集頻率,類似的還有 MaxMetaspaceFreeRatio

          七、Requested array size exceeds VM limit

          7.1 寫個 bug

          public?static?void?main(String[]?args)?{
          ??int[]?arr?=?new?int[Integer.MAX_VALUE];
          }

          這個比較簡單,建個超級大數(shù)組就會出現(xiàn) OOM,不多說了

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

          JVM 限制了數(shù)組的最大長度,該錯誤表示程序請求創(chuàng)建的數(shù)組超過最大長度限制。

          JVM 在為數(shù)組分配內(nèi)存前,會檢查要分配的數(shù)據(jù)結(jié)構(gòu)在系統(tǒng)中是否可尋址,通常為 Integer.MAX_VALUE-2

          此類問題比較罕見,通常需要檢查代碼,確認業(yè)務(wù)是否需要創(chuàng)建如此大的數(shù)組,是否可以拆分為多個塊,分批執(zhí)行。

          八、Out of swap space

          啟動 Java 應(yīng)用程序會分配有限的內(nèi)存。此限制是通過-Xmx和其他類似的啟動參數(shù)指定的。

          在 JVM 請求的總內(nèi)存大于可用物理內(nèi)存的情況下,操作系統(tǒng)開始將內(nèi)容從內(nèi)存換出到硬盤驅(qū)動器。

          3ae3da28d6b9bef7d5ac66a90c575e73.webp

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

          這種錯誤沒見過~~~

          九、Kill process or sacrifice child

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

          不同于其他的 OOM 錯誤, Killprocessorsacrifice child 錯誤不是由 JVM 層面觸發(fā)的,而是由操作系統(tǒng)層面觸發(fā)的。

          9.1 原因分析

          默認情況下,Linux 內(nèi)核允許進程申請的內(nèi)存總量大于系統(tǒng)可用內(nèi)存,通過這種“錯峰復(fù)用”的方式可以更有效的利用系統(tǒng)資源。

          然而,這種方式也會無可避免地帶來一定的“超賣”風險。例如某些進程持續(xù)占用系統(tǒng)內(nèi)存,然后導(dǎo)致其他進程沒有可用內(nèi)存。此時,系統(tǒng)將自動激活 OOM Killer,尋找評分低的進程,并將其“殺死”,釋放內(nèi)存資源。

          9.2 解決方案

          • 升級服務(wù)器配置/隔離部署,避免爭用
          • OOM Killer 調(diào)優(yōu)。

          最后附上一張“涯海”大神的圖

          2cc9bf3f6614911dfe1ad0b444104013.webp

          參考與感謝

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

          https://plumbr.io/outofmemoryerror

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

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


          ? ? ? ?54925cae240aa41a0e993dbb1281e401.webp???JVM內(nèi)存模型JVM GC算法
          不可不知的 7 個 JDK 命令

          覺得不錯,點個在看~

          4f8f23fd325c63f934b3a65841a83feb.webp
          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色大片免费在线观看 | 黄片影院黄片 | 国产乱伦免费 | 天天撸在线 | 欧美蜜桃亚洲 |