Java程序員必備:常見(jiàn)OOM異常分析
前言
放假這幾天,溫習(xí)了深入理解Java虛擬機(jī)的第二章, 整理了JVM發(fā)生OOM異常的幾種情況,并分析原因以及解決方案,希望對(duì)大家有幫助。
??
Java 堆溢出
Java堆用于存儲(chǔ)對(duì)象實(shí)例,只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾回收機(jī)制清除這些對(duì)象,那么在對(duì)象數(shù)量到達(dá)最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。
Java 堆溢出原因
無(wú)法在 Java 堆中分配對(duì)象
應(yīng)用程序保存了無(wú)法被GC回收的對(duì)象。
應(yīng)用程序過(guò)度使用 finalizer。
Java 堆溢出排查解決思路
1.查找關(guān)鍵報(bào)錯(cuò)信息,如
java.lang.OutOfMemoryError:Java heap space
2.使用內(nèi)存映像分析工具(如Eclipsc Memory Analyzer或者Jprofiler)對(duì)Dump出來(lái)的堆儲(chǔ)存快照進(jìn)行分析,分析清楚是內(nèi)存泄漏還是內(nèi)存溢出。
3.如果是內(nèi)存泄漏,可進(jìn)一步通過(guò)工具查看泄漏對(duì)象到GC Roots的引用鏈,修復(fù)應(yīng)用程序中的內(nèi)存泄漏。
4.如果不存在泄漏,先檢查代碼是否有死循環(huán),遞歸等,再考慮用 -Xmx 增加堆大小。
demo代碼
package oom;import java.util.ArrayList;import java.util.List;/*** JVM配置參數(shù)* -Xms20m JVM初始分配的內(nèi)存20m* -Xmx20m JVM最大可用內(nèi)存為20m* -XX:+HeapDumpOnOutOfMemoryError 當(dāng)JVM發(fā)生OOM時(shí),自動(dòng)生成DUMP文件* -XX:HeapDumpPath=/Users/weihuaxiao/Desktop/dump/ 生成DUMP文件的路徑*/publicclassHeapOOM{staticclassOOMObject{}publicstaticvoid main(String[] args){List<OOMObject> list =newArrayList<OOMObject>();//在堆中無(wú)限創(chuàng)建對(duì)象while(true){list.add(newOOMObject());}}}
運(yùn)行結(jié)果

按照前面的排查解決方案,我們來(lái)一波分析。
1.查找報(bào)錯(cuò)關(guān)鍵信息
Exceptionin thread "main" java.lang.OutOfMemoryError:Java heap space
2. 使用內(nèi)存映像分析工具Jprofiler分析產(chǎn)生的堆儲(chǔ)存快照

由圖可得,OOMObject這個(gè)類創(chuàng)建了810326個(gè)實(shí)例,是屬于內(nèi)存溢出,這時(shí)候先定位到對(duì)應(yīng)代碼,發(fā)現(xiàn)死循環(huán)導(dǎo)致的,修復(fù)即可。
棧溢出
關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError 異常;
如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。
棧溢出原因
在單個(gè)線程下,棧幀太大,或者虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無(wú)法分配的時(shí)候,虛擬機(jī)拋出StackOverflowError 異常。
不斷地建立線程的方式會(huì)導(dǎo)致內(nèi)存溢出。
棧溢出排查解決思路
查找關(guān)鍵報(bào)錯(cuò)信息,確定是StackOverflowError還是OutOfMemoryError
如果是StackOverflowError,檢查代碼是否遞歸調(diào)用方法等
如果是OutOfMemoryError,檢查是否有死循環(huán)創(chuàng)建線程等,通過(guò)-Xss降低的每個(gè)線程棧大小的容量
demo代碼
package oom;/*** -Xss2M*/publicclassJavaVMStackOOM{privatevoid dontStop(){while(true){}}publicvoid stackLeakByThread(){while(true){Thread thread =newThread(newRunnable(){publicvoid run(){dontStop();}});thread.start();}}publicstaticvoid main(String[] args){JavaVMStackOOM oom =newJavaVMStackOOM();oom.stackLeakByThread();}}
運(yùn)行結(jié)果

1.查找報(bào)錯(cuò)關(guān)鍵信息
Exceptionin thread "main" java.lang.OutOfMemoryError: unable to create newnative thread
2.確定是創(chuàng)建線程導(dǎo)致的棧溢出OOM
Thread thread =newThread(newRunnable(){publicvoid run(){dontStop();}});
方法區(qū)溢出
方法區(qū),(又叫永久代,JDK8后,元空間替換了永久代),用于存放Class的相關(guān)信息,如類名、訪問(wèn)修飾符、常量池、字段描述、方法描述等。運(yùn)行時(shí)產(chǎn)生大量的類,會(huì)填滿方法區(qū),造成溢出。
方法區(qū)溢出原因
使用CGLib生成了大量的代理類,導(dǎo)致方法區(qū)被撐爆
在Java7之前,頻繁的錯(cuò)誤使用String.intern方法
大量jsp和動(dòng)態(tài)產(chǎn)生jsp
應(yīng)用長(zhǎng)時(shí)間運(yùn)行,沒(méi)有重啟
方法區(qū)溢出排查解決思路
檢查是否永久代空間設(shè)置得過(guò)小
檢查代碼是否頻繁錯(cuò)誤得使用String.intern方法
檢查是否跟jsp有關(guān)。
檢查是否使用CGLib生成了大量的代理類
重啟大法,重啟JVM
demo代碼
package oom;import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/*** jdk8以上的話,* 虛擬機(jī)參數(shù):-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M*/publicclassJavaMethodAreaOOM{publicstaticvoid main(String[] args){while(true){Enhancer enhancer =newEnhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(newMethodInterceptor(){publicObject intercept(Object obj,Method method,Object[] args,MethodProxy proxy)throwsThrowable{return proxy.invokeSuper(obj, args);}});enhancer.create();}}staticclassOOMObject{}}
運(yùn)行結(jié)果

1.查找報(bào)錯(cuò)關(guān)鍵信息
Causedby: java.lang.OutOfMemoryError:Metaspace
2.檢查JVM元空間設(shè)置參數(shù)是否過(guò)小
-XX:MetaspaceSize=10M-XX:MaxMetaspaceSize=10M
3. 檢查對(duì)應(yīng)代碼,是否使用CGLib生成了大量的代理類
while(true){...enhancer.setCallback(newMethodInterceptor(){publicObject intercept(Object obj,Method method,Object[] args,MethodProxy proxy)throwsThrowable{return proxy.invokeSuper(obj, args);}});enhancer.create();}
本機(jī)直接內(nèi)存溢出
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是,這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OOM。
在JDK1.4 中新加入了NIO(New Input/Output)類,它可以使用 native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。
直接內(nèi)存溢出原因
本機(jī)直接內(nèi)存的分配雖然不會(huì)受到Java 堆大小的限制,但是受到本機(jī)總內(nèi)存大小限制。
直接內(nèi)存由 -XX:MaxDirectMemorySize 指定,如果不指定,則默認(rèn)與Java堆最大值(-Xmx指定)一樣。
NIO程序中,使用ByteBuffer.allocteDirect(capability)分配的是直接內(nèi)存,可能導(dǎo)致直接內(nèi)存溢出。
直接內(nèi)存溢出
檢查代碼是否恰當(dāng)
檢查JVM參數(shù)-Xmx,-XX:MaxDirectMemorySize 是否合理。
demo代碼
package oom;import java.nio.ByteBuffer;import java.util.concurrent.TimeUnit;/*** -Xmx256m -XX:MaxDirectMemorySize=100M*/publicclassDirectByteBufferTest{publicstaticvoid main(String[] args)throwsInterruptedException{//分配128MB直接內(nèi)存ByteBuffer bb =ByteBuffer.allocateDirect(1024*1024*128);TimeUnit.SECONDS.sleep(10);System.out.println("ok");}}
運(yùn)行結(jié)果

ByteBuffer分配128MB直接內(nèi)存,而JVM參數(shù)-XX:MaxDirectMemorySize=100M指定最大是100M,因此發(fā)生直接內(nèi)存溢出。
ByteBuffer bb =ByteBuffer.allocateDirect(1024*1024*128);
??
GC overhead limit exceeded
這個(gè)是JDK6新加的錯(cuò)誤類型,一般都是堆太小導(dǎo)致的。
Sun 官方對(duì)此的定義:超過(guò)98%的時(shí)間用來(lái)做GC并且回收了不到2%的堆內(nèi)存時(shí)會(huì)拋出此異常。
解決方案
檢查項(xiàng)目中是否有大量的死循環(huán)或有使用大內(nèi)存的代碼,優(yōu)化代碼。
檢查JVM參數(shù)-Xmx -Xms是否合理
dump內(nèi)存,檢查是否存在內(nèi)存泄露,如果沒(méi)有,加大內(nèi)存。
demo代碼
package oom;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/*** JVm參數(shù) -Xmx8m -Xms8m*/publicclassGCoverheadTest{publicstaticvoid main(String[] args){ExecutorService executor =Executors.newFixedThreadPool(10);for(int i =0; i <Integer.MAX_VALUE; i++){executor.execute(()->{try{Thread.sleep(10000);}catch(InterruptedException e){//do nothing}});}}}
運(yùn)行結(jié)果
實(shí)例代碼使用了newFixedThreadPool線程池,它使用了無(wú)界隊(duì)列,無(wú)限循環(huán)執(zhí)行任務(wù),會(huì)導(dǎo)致內(nèi)存飆升。因?yàn)樵O(shè)置了堆比較小,所以出現(xiàn)此類型OOM。
??
總結(jié)
本文介紹了以下幾種常見(jiàn)OOM異常
java.lang.OutOfMemoryError:Java heap spacejava.lang.OutOfMemoryError: unable to create newnative threadjava.lang.OutOfMemoryError:Metaspacejava.lang.OutOfMemoryError:Direct buffer memoryjava.lang.OutOfMemoryError: GC overhead limit exceeded
希望大家遇到OOM異常時(shí),對(duì)癥下藥,順利解決問(wèn)題。同時(shí),如果有哪里寫(xiě)得不對(duì),歡迎指出,感激不盡。?
參考與感謝
JVM系列之實(shí)戰(zhàn)內(nèi)存溢出異常
JVM 發(fā)生 OOM 的 8 種原因、及解決辦法
NIO-直接內(nèi)存
《深入理解Java虛擬機(jī)》
有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
