在《Java虛擬機規范》的教寫(xiě)規定里,除了程序計數器外,常常分┐(′?`)┌虛擬機內存的教寫(xiě)其他幾個(gè)運行時(shí)區域都有發(fā)生 OutO(′?_?`)fMemoryError 異常的可能。本篇主(′Д` )要包括如下 OOM 的常常分介紹和示例:java.lang.Stack??OverflowErrorjava.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.l??ang.OutO??fMemoryError:Metaspacej(′?_?`)ava.lang.OutOfMemoryError: Requested array size exceeds VM limitjava.lang.OutOfMemoryError: Out of swaヾ(′▽?zhuān)??p spacejava.lang.OutOfMemoryError:Kill process or sacrifice child我們常說(shuō)的 OOM 異常,其實(shí)是教寫(xiě) Error一. Stack(//ω//)OverflowError1.1 寫(xiě)個(gè) bugpublic class StackOverflowErrorDemo { public static void main(String[](???) args) { javaKeeper(); } private static void javaKeeper() { javaKee??per(); }}上一篇詳細的介紹過(guò)JVM 運行時(shí)數據區,JVM 虛擬機棧是常常分有深度的,在執行方法的教寫(xiě)時(shí)候會(huì )伴隨著(zhù)入棧和出棧,上邊的常常分方法可以看到,ma??in 方法執行后不停的教寫(xiě)遞歸,ヽ(′ー`)ノ遲早把棧撐爆了Exception in thread "main" java.lang.StackOverflowError at oo??m.Stack(′▽?zhuān)?O??ve???rflowErr(′_ゝ`)orDemo.javaKeeper(StackOverflowErrorDemo??.java:15)1.2 原因分析無(wú)限遞歸循環(huán)調用(最常見(jiàn)原因),常常分要時(shí)刻注意代碼中是教寫(xiě)否有了循環(huán)調用方法而無(wú)法退出的情況執行了大量??方法,導致線(xiàn)程??臻g耗盡方法內聲明了海量的常常分局部變量native 代碼有棧上分配的邏輯,并且要求的教寫(xiě)內存還不小,比如 java.net.Soc(╬ ò﹏ó)ketInputStream.read0 會(huì )在棧上要求ヾ(′?`)?分配一個(gè) 64KB 的常常分緩存(64位 Linux)1.3 解決方案修復引發(fā)無(wú)限遞歸調用的異常代碼, 通過(guò)程序拋出ヽ(′▽?zhuān)?ノ的教寫(xiě)異常堆棧,找出不斷重復的??代碼??行,按圖索驥,修復無(wú)限遞歸 Bug排查是否存在類(lèi)之間的循環(huán)依賴(lài)(當兩個(gè)對象相互引用,在調用toStri??ng方法時(shí)也會(huì )產(chǎn)生這個(gè)異常)通過(guò) JVM 啟動(dòng)參數 -Xss 增加線(xiàn)程棧內存空間, 某些正常使用場(chǎng)景需要執行大(da)量方法或包含大量局部變量,這時(shí)可以適當地提高線(xiàn)程??臻g限制二(′?ω?`). Java?? heap space(′?`)Java 堆用于存儲對象實(shí)例,我們只要不斷的創(chuàng )建對象,并且保證 GC Roots 到對象之間有可達路徑來(lái)避免 GC 清除這些對象,那隨著(zhù)對象數量的增加,總容量觸及堆的最大容量限制后就??會(huì )產(chǎn)生內存溢出異常。Java 堆內存的 OOM 異常是實(shí)際應ヾ(′▽?zhuān)??用中最常見(jiàn)的內存溢出異常。2.1 寫(xiě)個(gè) bug/(′?ω?`)** * JVM參數:-Xmx12m */public class JavaHeapSpaceDemo { static final int SIZE = 2 * 1024 * 1024; public?? statヾ(′ω`)?ic void main(String[](′?_?`) a) { int[] i = new int; }}代碼試圖分配(′_ゝ`)容量為 2M 的 int 數組,如果指定啟動(dòng)參數 -Xmx12m,分配內存??就不夠用,就類(lèi)似于將 XXXL 號的對象,往 S 號的 Java heap space 里面塞。Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at oom.JavaHeapSpaceDemo.main(JavaHeapヾ(′▽?zhuān)??SpaceDemo.java:13)2.2 原因分析請求創(chuàng )建一個(gè)超大對象,通常是一個(gè)大數組超出預期的訪(fǎng)問(wèn)量/數據量,通常是上游系統請求流量飆升,常見(jiàn)于各類(lèi)促銷(xiāo)/秒殺活動(dòng),可以結合業(yè)(ye)務(wù)流量指標排查是否有尖狀峰值過(guò)度使用終結器(Finalizer),該對象沒(méi)有立即被 GC內存泄漏(Memory Leak),大量對象引用沒(méi)有釋放,JVM 無(wú)法對其自動(dòng)回收,常見(jiàn)于使(shi)用了 File 等資源沒(méi)有回收2.3 解決方案針對大部分??情況,通常只需要通過(guò) -Xmx 參數調高 JVM 堆內存空間即可(ke)。如果仍然沒(méi)有解決,可以參考以下情況做進(jìn)一步處理:如果是超大對象,可以檢查其合理性,比如是否一次性查詢(xún)了數據庫全部結果,而沒(méi)有做結果數限制如果是業(yè)務(wù)(wu)峰值壓力,可以考慮添加機器資源,或者做限流降級。如果是內存(cun)泄漏,需要ヽ(′▽?zhuān)?/找到持有的對象,修改代碼設計,比如關(guān)閉沒(méi)有釋放的連接面試官:說(shuō)說(shuō)內存泄露和內存溢出加送個(gè)知識點(diǎn),三連的終將成為大神~~內存泄露和內存溢出內存溢出(out of memory),是指程序在申請內存時(shí),沒(méi)有足夠的內存空間供其使用,出現ou??t of memory;比如申請了一個(gè) Integer,但給它存了 Long 才能存下的數,那就是內存溢出。內存泄露( memory leak),是指程序在申請內存后,無(wú)法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很?chē)乐?,無(wú)論多?少內存,遲早會(huì )被占光。memory leak 最終會(huì )導致 out of memory!三、GC overhead lim??it exceededJVM 內置了垃圾回收機制GC,所以作為 Javaer(′▽?zhuān)?) 的我們不需要手工編寫(xiě)代碼??來(lái)進(jìn)行內存分配和釋放,但是當 Java 進(jìn)程花費 98% 以上的時(shí)間執行 GC,但只恢復了不到 2% 的內存,且該動(dòng)作連續重復了 5 次,就會(huì )拋出 java.lang.OutOfMemoryError:GC overhead limit ex(???)ceeded 錯誤(俗稱(chēng):垃圾回收上頭)。簡(jiǎn)單地說(shuō),就是應用程序已經(jīng)基本耗盡了所有可用內存, GC 也無(wú)法回收。假如不拋出 GC overhead limit exceeded 錯誤,那 GC 清理的那么一??丟丟內存很快就會(huì )被再次填滿(mǎn),迫使 GC 再次執行,這樣惡性循環(huán),CPU 使用率 100%,而 GC?? 沒(méi)什??么效果。3.1 寫(xiě)個(gè) bug出現這個(gè)錯誤的實(shí)例,其實(shí)我們寫(xiě)個(gè)無(wú)限循環(huán),往 List 或 Map 加(jia)數據就會(huì )一直 Full GC,直到扛不住,這里用一個(gè)ヾ(^-^)ノ不容易發(fā)現的栗子。我們往 map 中添加 1000 個(gè)元素。/** * JVM 參數:(◎_◎;) -Xmx14m -XX:+PrintGCDetails */p( ?ヮ?)ublic 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 (i??nt 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()=54000m.size()=55000m.size()=56000Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded從輸出結果可以看到,我們的限制 1000 條數據沒(méi)有起作用,map 容量遠超過(guò)了 1000,而且最后也出現了我們想要的錯誤,這是因為類(lèi) Key 只重寫(xiě)了 hashCode() 方法,卻沒(méi)有重寫(xiě) equals() 方法,我們在使用 containsKey() 方法其實(shí)就出現了問(wèn)題,于是就會(huì )一直往 HashMap 中添加 Key,直至 GC 都清理不掉。 ? 面試官又來(lái)了:說(shuō)一下HashMap原理以及為什么需要同時(shí)實(shí)現equals和hashcode執行這個(gè)程序的最終錯誤,和 JVM 配置也會(huì )有關(guān)系,如果設置的堆內存特別小,會(huì )直接報 Java heap space。算是被這個(gè)錯誤截胡了,所以有時(shí),在資源受限的情況下,無(wú)法準確預測程序會(huì )死于哪種具體的原因。3.2 解決方案添加 JVM 參數-XX:-UseGCOverheadLimit 不推薦這么干,沒(méi)有真正解決問(wèn)題,只是將異常推遲檢查項目中是否有大量的死循環(huán)或有使用大內存的代碼,優(yōu)化代碼dump內存分析,檢查是否存在內存泄露,如果沒(méi)有,加大內存四、Direct buffer memory我們使用 NIO 的時(shí)候經(jīng)常需要使用 ByteBuffer 來(lái)讀取或寫(xiě)入數據,這是一種基于 Channel(通道) 和 Buffer(緩沖區)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然后通過(guò)一個(gè)存儲在 Java 堆里面的 DirectByteBuffer 對象作為這塊內存的引用進(jìn)行操作。這樣在一些場(chǎng)景就避免了 Java 堆和 Native 中來(lái)回復制數據,所以性能會(huì )有所提高。Java 允許應用程序通過(guò) Direct ByteBuffer 直接訪(fǎng)問(wèn)堆外內存,許多高性能程序通過(guò) Direct ByteBuffer 結合內存映射文件(Memory Mapped File)實(shí)現高速 IO。4.1 寫(xiě)個(gè) bugByteBuffer.allocate(capability) 是分配 JVM 堆內存,屬于 GC 管轄范圍,需要內存拷貝所以速度相對較慢;ByteBuffer.allocateDirect(capability) 是分配 OS 本地內存,不屬于 GC 管轄范圍,由于不需要內存拷貝所以速度相對較快;如果不斷分配本地內存,堆內存很少使用,那么 JVM 就不需要執行 GC,DirectByteBuffer 對象就不會(huì )被回收,這時(shí)雖然堆內存充足,但本地內存可能已經(jīng)不夠用了,就會(huì )出現 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,所以我們設小點(diǎn),然后使用直接內存超過(guò)這個(gè)值,就會(huì )出現 OOM。maxDirectMemory is:5MBException in thread "main" java.lang.OutOfMemoryError: Direct buffer memory4.2 解決方案Java 只能通過(guò) ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過(guò) Arthas 等在線(xiàn)診斷工具攔截該方法進(jìn)行排查檢查是否直接或間接使用了 NIO,如 netty,jetty 等通過(guò)啟動(dòng)參數 -XX:MaxDirectMemorySize 調整 Direct ByteBuffer 的上限值檢查 JVM 參數是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數會(huì )使 System.gc() 失效檢查堆外內存使用代碼,確認是否存在內存泄漏;或者通過(guò)反射調用 sun.misc.Cleaner 的 clean() 方法來(lái)主動(dòng)釋放被 Direct ByteBuffer 持有的內存空間內存容量確實(shí)不足,升級配置五、Unable to create new native thread每個(gè) Java 線(xiàn)程都需要占用一定的內存空間,當 JVM 向底層操作系統請求創(chuàng )建一個(gè)新的 native 線(xiàn)程時(shí),如果沒(méi)有足夠的資源分配就會(huì )報此類(lèi)錯誤。5.1 寫(xiě)個(gè) bugpublic static void main(String[] args) { while(true){ new Thread(() -> { try { Th(′_`)rea(//ω//)d.sleep(Integer.MAX_VALUE??(╯°□°)╯); } catch(Interrupt(′;ω;`)edException e) { } }).start(); }}ヽ(′?`)ノError occurred during initialization of VMjava.lang.OutOfMemoryError: unable to create new native thread5.2?? 原因分析JVM 向 OS 請求創(chuàng )ˉ\_(ツ)_/ˉ建 native 線(xiàn)程失敗,就會(huì )拋出 Unableto createnewnativethread,常見(jiàn)的原因包括以下幾類(lèi):線(xiàn)程數超過(guò)操作系統最大線(xiàn)程數限制(和平臺有關(guān))線(xiàn)ヽ(′ー`)ノ程數超過(guò) kernel.pid_max(只能重啟)native 內存不足;該問(wèn)題發(fā)生的常見(jiàn)過(guò)程主要包括以下幾步:JVM 內部的應用程序請求創(chuàng )建一個(gè)新的 Java 線(xiàn)程;JVM native 方法代理??了該次請求,并向操作系統請求創(chuàng )建一個(gè) native 線(xiàn)程;操作系統嘗試創(chuàng )建一個(gè)新的 native 線(xiàn)程,并為其分配內存;如果操作系統??的虛擬內存已耗盡,或是受到 32 位進(jìn)程的地址空間限制,操作系統就會(huì )拒絕本次 native 內存分配;JV(╬?益?)M 將拋出 java.lang.OutOfMem??oryError:Unableto createnewnativethread 錯誤。5.3 解決方案想辦法降低程序中創(chuàng )建ヾ(′▽?zhuān)??線(xiàn)程的數量,分析應用是否真的需要創(chuàng )建這么多線(xiàn)程如果確實(shí)需要創(chuàng )建很多線(xiàn)程,調高 OS 層面的線(xiàn)程最大數:執行 ulimヽ(′▽?zhuān)?/ia-a 查看最大線(xiàn)程數限制,使用 ulimit-u xxx 調整最大線(xiàn)程數限制六、MetaspaceJDK 1.8 之前會(huì )出現 Permgen space,該錯誤表示永久代(Permanent Generation)已用滿(mǎn),通常是因為加載的 class 數目太多或體積太大。隨著(zhù) 1.8 中永久代的取消,就不會(huì )出現這種異常了。Metaspace 是方法區在 HotSpot 中的(de)實(shí)現,它與永久代最大的區別在于??,元空間并不在虛擬機內存中而是使用本地內存,但是本地內存也有打滿(mǎn)的時(shí)候,所以也會(huì )有異常。6.1 寫(xiě)個(gè) 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); enh(⊙_⊙)ancer.set??U(′?_?`)seCache(false); enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> { //動(dòng)態(tài)代理創(chuàng )建對象 return methodProxy.invokeSuper(o, objects); }); enhancer.create(); } }}借助 Spring 的 GCLib 實(shí)現動(dòng)態(tài)創(chuàng )建對象Exception in thread "main" org.spr( ?ヮ?)ing(′?`)framework.cglib.core.CodeGenerationException: java.lang.??OutOfMemoryError-->M??etaspace6.2 解決方案方法區溢出也是一種常見(jiàn)的內存溢出異常,在經(jīng)常運行時(shí)生成大量動(dòng)態(tài)類(lèi)的應用場(chǎng)景中,就應該特別關(guān)注這些類(lèi)的回收情(qing)況。這類(lèi)場(chǎng)景除了上邊的 GCLib 字節碼增強和動(dòng)態(tài)語(yǔ)言外,常見(jiàn)的還有,大量 JSP 或動(dòng)態(tài)(╬?益?)產(chǎn)生 JSP 文件的應(ying)用(遠古時(shí)代的傳統軟件行業(yè)可能會(huì )有)、基于 OSGi 的應用(即使同一(′ω`)個(gè)類(lèi)文件,被不同的加載器加載也會(huì )視為(′▽?zhuān)?)不同的類(lèi))等。方法區在 JDK8 中一般不太容易產(chǎn)生,HotSpot 提供了一些參數來(lái)設置元空間,可(ke)以起到預防作用-XX:MaxMetaspaceSize 設置元空間最大值(?_?;),默認是 -1,表示不限制(還是要受本地內存大(′▽?zhuān)?小限制的)-XX:MetaspaceSize 指定元空間的初始空間大小,以字節為單位,達到該值就會(huì )觸發(fā) GC 進(jìn)行類(lèi)型卸載,同時(shí)收集器會(huì )對該值進(jìn)行調整-XX:MinMetaspac(′▽?zhuān)?eFreeRatio 在 GC 之后控制最小的元空間剩余容量的百分比,可減少因(yin)元空間不??足導致的垃圾收集頻率,類(lèi)似的還有 MaxMetaspaceFreeRatio七、Requested array size(?????) exc??eeds VM limit7.1 寫(xiě)個(gè) bugpublic static void main(String[(′Д` )] args) { int[] arr = new int;}這??個(gè)比較簡(jiǎn)??單,建個(gè)超級大數組就會(huì )出現 OOM,不多說(shuō)了Exception in thread "main" java.(′?_?`)lang.OutOfMemoryError: Requested array size exceeds VM limitJVM 限制了數組的最大長(cháng)度,該錯誤表(biao)示程序請求創(chuàng )??建的數組超過(guò)最大長(cháng)度限制。JVM 在為數組分??配內存前,會(huì )檢查要分配的數據結構在系統中是否可尋址,通常為 Integer.MAX_VALUE-2。此類(lèi)問(wèn)題比較罕見(jiàn),通常需要檢查代碼,確認業(yè)務(wù)是否需要創(chuàng )建如此大的數組,是否可以拆分為多個(gè)塊,分批執行。八、Out of swap space啟動(dòng) Java 應用程序會(huì )分配有限的內存。此限制是通過(guò)-X??mx和其他類(lèi)似的啟動(dòng)參數指定的。在 JVM 請求的總內存大于可用物理內存的情況下,操作系統開(kāi)始將內容從內存換出到硬盤(pán)驅動(dòng)器。該錯誤表示所有可用的虛擬內存已被耗盡。虛擬內存(Virtual Memory)由物理內??存(Physical Memory)和交換空間(Swap Space)兩部分組成。這種錯誤沒(méi)見(jiàn)過(guò)~~~九、Kill process or sacrifice child操作系統是建立在流程概念之上的。這些進(jìn)程由幾個(gè)內核作業(yè)負責,其中一個(gè)名為“ Out of memory Killer”,它會(huì )在可用內存極低的情況下“殺死”(kill)某些進(jìn)程。OOM Killer 會(huì )對所有??進(jìn)程進(jìn)行打分,然后將評分較低(′▽?zhuān)?的進(jìn)程“殺死”,具體的評分規則可以參考 Surviving the Linux OOM Killer。不同于其他的 OOM 錯誤, Killproce??ssorsacrific(?Д?)e child 錯誤不是由 JVM 層面觸發(fā)的,而是由操作系統層面觸發(fā)的。9.1 原因分析默認情況下,Linux 內核允許進(jìn)程申請的內存總量大于系???統可用內存,通過(guò)這種“錯峰復用”的方式可以更有效的利用系統資源。然而,這種方式也會(huì )無(wú)可避免地帶來(lái)一定的“超賣(mài)”風(fēng)險。例如某些進(jìn)程持續占用系統內存,然后導致其(′?_?`)他(ta)進(jìn)程沒(méi)( ?▽?)有可用內存。此時(shí),系統將自動(dòng)激活 OOM Killer,尋找評分低的進(jìn)程,并將??其“殺死”,釋放內存資源。9.2 解決方案升級服務(wù)器配置/隔離部署,避免爭用OOM Killer 調優(yōu)。最后附上一張“涯?!贝笊竦膱D參考與感謝《深入理解 Java 虛擬機 第 3 版》https://plumbr.io/outofmemoryerro??rhttps://yq.aliyun.com/articles/711191https://github.com/StabilityMan/StabilityGuide/blob/mast??er/docs/diagnosis/jvm/exception完 ●JVM內存???模型●JVM GC算法●不可不知的 7 個(gè) JDK 命令覺(jué)得不錯,點(diǎn)個(gè)在看~