快速定位Java 內(nèi)存OOM的問(wèn)題
Java服務(wù)出現(xiàn)了OOM(Out Of Memory)問(wèn)題,總結(jié)了一些相對(duì)通用的方案,希望能幫助到Java技術(shù)棧的同學(xué)。
某Java服務(wù)(假設(shè)PID=10765)出現(xiàn)了OOM,最常見(jiàn)的原因?yàn)椋?/strong>
有可能是內(nèi)存分配確實(shí)過(guò)小,而正常業(yè)務(wù)使用了大量?jī)?nèi)存
某一個(gè)對(duì)象被頻繁申請(qǐng),卻沒(méi)有釋放,內(nèi)存不斷泄漏,導(dǎo)致內(nèi)存耗盡
某一個(gè)資源被頻繁申請(qǐng),系統(tǒng)資源耗盡,例如:不斷創(chuàng)建線程,不斷發(fā)起網(wǎng)絡(luò)連接
畫(huà)外音:無(wú)非“本身資源不夠”“申請(qǐng)資源太多”“資源耗盡”幾個(gè)原因。
更具體的,可以使用以下工具逐一排查。
一、確認(rèn)是不是內(nèi)存本身就分配過(guò)小
方法:jmap -heap 10765

如上圖,可以查看新生代,老生代堆內(nèi)存的分配大小以及使用情況,看是否本身分配過(guò)小。
二、找到最耗內(nèi)存的對(duì)象
方法:jmap -histo:live 10765 | more

如上圖,輸入命令后,會(huì)以表格的形式顯示存活對(duì)象的信息,并按照所占內(nèi)存大小排序:
實(shí)例數(shù)
所占內(nèi)存大小
類名
是不是很直觀?對(duì)于實(shí)例數(shù)較多,占用內(nèi)存大小較多的實(shí)例/類,相關(guān)的代碼就要針對(duì)性review了。
上圖中占內(nèi)存最多的對(duì)象是RingBufferLogEvent,共占用內(nèi)存18M,屬于正常使用范圍。
如果發(fā)現(xiàn)某類對(duì)象占用內(nèi)存很大(例如幾個(gè)G),很可能是類對(duì)象創(chuàng)建太多,且一直未釋放。例如:
申請(qǐng)完資源后,未調(diào)用close()或dispose()釋放資源
消費(fèi)者消費(fèi)速度慢(或停止消費(fèi)了),而生產(chǎn)者不斷往隊(duì)列中投遞任務(wù),導(dǎo)致隊(duì)列中任務(wù)累積過(guò)多
畫(huà)外音:線上執(zhí)行該命令會(huì)強(qiáng)制執(zhí)行一次fgc。另外還可以dump內(nèi)存進(jìn)行分析。
三、確認(rèn)是否是資源耗盡
工具:
pstree
netstat
查看進(jìn)程創(chuàng)建的線程數(shù),以及網(wǎng)絡(luò)連接數(shù),如果資源耗盡,也可能出現(xiàn)OOM。
這里介紹另一種方法,通過(guò)
/proc/${PID}/fd
/proc/${PID}/task
可以分別查看句柄詳情和線程數(shù)。
例如,某一臺(tái)線上服務(wù)器的sshd進(jìn)程PID是9339,查看
ll /proc/9339/fd
ll /proc/9339/task

如上圖,sshd共占用了四個(gè)句柄
0 -> 標(biāo)準(zhǔn)輸入
1 -> 標(biāo)準(zhǔn)輸出
2 -> 標(biāo)準(zhǔn)錯(cuò)誤輸出
3 -> socket(容易想到是監(jiān)聽(tīng)端口)
sshd只有一個(gè)主線程PID為9339,并沒(méi)有多線程。
所以,只要
ll /proc/${PID}/fd | wc -l
ll /proc/${PID}/task | wc -l (效果等同pstree -p | wc -l)
就能知道進(jìn)程打開(kāi)的句柄數(shù)和線程數(shù)。
補(bǔ)充:Java內(nèi)存溢出OOM
Java內(nèi)存溢出OOM
經(jīng)典錯(cuò)誤
JVM中常見(jiàn)的兩個(gè)錯(cuò)誤
StackoverFlowError :棧溢出
OutofMemoryError: java heap space:堆溢出
除此之外,還有以下的錯(cuò)誤
java.lang.StackOverflowError java.lang.OutOfMemoryError:java heap space java.lang.OutOfMemoryError:GC overhead limit exceeeded java.lang.OutOfMemoryError:Direct buffer memory java.lang.OutOfMemoryError:unable to create new native thread java.lang.OutOfMemoryError:Metaspace
架構(gòu)
OutOfMemoryError和StackOverflowError是屬于Error,不是Exception

StackoverFlowError
堆棧溢出,我們有最簡(jiǎn)單的一個(gè)遞歸調(diào)用,就會(huì)造成堆棧溢出,也就是深度的方法調(diào)用
棧一般是512K,不斷的深度調(diào)用,直到棧被撐破
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
/**
* 棧一般是512K,不斷的深度調(diào)用,直到棧被撐破
* Exception in thread "main" java.lang.StackOverflowError
*/
private static void stackOverflowError() {
stackOverflowError();
}
}
運(yùn)行結(jié)果
Exception in thread "main" java.lang.StackOverflowError at com.moxi.interview.study.oom.StackOverflowErrorDemo.stackOverflowError(StackOverflowErrorDemo.java:17)
OutOfMemoryError
java heap space
創(chuàng)建了很多對(duì)象,導(dǎo)致堆空間不夠存儲(chǔ)
/**
* Java堆內(nèi)存不足
*/
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
// 堆空間的大小 -Xms10m -Xmx10m
// 創(chuàng)建一個(gè) 80M的字節(jié)數(shù)組
byte [] bytes = new byte[80 * 1024 * 1024];
}
}
我們創(chuàng)建一個(gè)80M的數(shù)組,會(huì)直接出現(xiàn)Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
GC overhead limit exceeded
GC回收時(shí)間過(guò)長(zhǎng)時(shí)會(huì)拋出OutOfMemoryError,過(guò)長(zhǎng)的定義是,超過(guò)了98%的時(shí)間用來(lái)做GC,并且回收了不到2%的堆內(nèi)存
連續(xù)多次GC都只回收了不到2%的極端情況下,才會(huì)拋出。假設(shè)不拋出GC overhead limit 錯(cuò)誤會(huì)造成什么情況呢?
那就是GC清理的這點(diǎn)內(nèi)存很快會(huì)再次被填滿,迫使GC再次執(zhí)行,這樣就形成了惡性循環(huán),CPU的使用率一直都是100%,而GC卻沒(méi)有任何成果。

代碼演示:
為了更快的達(dá)到效果,我們首先需要設(shè)置JVM啟動(dòng)參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
這個(gè)異常出現(xiàn)的步驟就是,我們不斷的像list中插入String對(duì)象,直到啟動(dòng)GC回收
/**
* GC 回收超時(shí)
* JVM參數(shù)配置: -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class GCOverheadLimitDemo {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while(true) {
//1.6時(shí)intern()方法發(fā)現(xiàn)字符串常量池(存儲(chǔ)永久代)沒(méi)有就復(fù)制,物理拷貝
//1.7時(shí)intern()方法發(fā)現(xiàn)字符串常量池(存儲(chǔ)堆)沒(méi)有就在保存地址值映射實(shí)際堆內(nèi)存對(duì)象
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
} finally {
}
}
}
運(yùn)行結(jié)果
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7106K->7106K(7168K)] 9154K->9154K(9728K), [Metaspace: 3504K->3504K(1056768K)], 0.0311093 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7136K->667K(7168K)] 9184K->667K(9728K), [Metaspace: 3540K->3540K(1056768K)], 0.0058093 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap PSYoungGen total 2560K, used 114K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000) eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1c878,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 7168K, used 667K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000) object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6a6ff8,0x00000000ffd00000) Metaspace used 3605K, capacity 4540K, committed 4864K, reserved 1056768K class space used 399K, capacity 428K, committed 512K, reserved 1048576K Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.Integer.toString(Integer.java:403) at java.lang.String.valueOf(String.java:3099) at com.moxi.interview.study.oom.GCOverheadLimitDemo.main(GCOverheadLimitDemo.java:18)
我們能夠看到 多次Full GC,并沒(méi)有清理出空間,在多次執(zhí)行GC操作后,就拋出異常 GC overhead limit
Direct buffer memory
Netty + NIO:這是由于NIO引起的
寫(xiě)NIO程序的時(shí)候經(jīng)常會(huì)使用ByteBuffer來(lái)讀取或?qū)懭霐?shù)據(jù),這是一種基于通道(Channel) 與 緩沖區(qū)(Buffer)的I/O方式,它可以使用Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆里面的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。
ByteBuffer.allocate(capability):第一種方式是分配JVM堆內(nèi)存,屬于GC管轄范圍,由于需要拷貝所以速度相對(duì)較慢
ByteBuffer.allocteDirect(capability):第二種方式是分配OS本地內(nèi)存,不屬于GC管轄范圍,由于不需要內(nèi)存的拷貝,所以速度相對(duì)較快
但如果不斷分配本地內(nèi)存,堆內(nèi)存很少使用,那么JVM就不需要執(zhí)行GC,DirectByteBuffer對(duì)象就不會(huì)被回收,這時(shí)候堆內(nèi)存充足,但本地內(nèi)存可能已經(jīng)使用光了,再次嘗試分配本地內(nèi)存就會(huì)出現(xiàn)OutOfMemoryError,那么程序就崩潰了。
一句話說(shuō):本地內(nèi)存不足,但是堆內(nèi)存充足的時(shí)候,就會(huì)出現(xiàn)這個(gè)問(wèn)題
我們使用 -XX:MaxDirectMemorySize=5m 配置能使用的堆外物理內(nèi)存為5M
-Xms20m -Xmx20m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
然后我們申請(qǐng)一個(gè)6M的空間
// 只設(shè)置了5M的物理內(nèi)存使用,但是卻分配 6M的空間 ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
這個(gè)時(shí)候,運(yùn)行就會(huì)出現(xiàn)問(wèn)題了
配置的maxDirectMemory:5.0MB
[GC (System.gc()) [PSYoungGen: 2030K->488K(2560K)] 2030K->796K(9728K), 0.0008326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 308K->712K(7168K)] 796K->712K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0052052 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:693) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at com.moxi.interview.study.oom.DIrectBufferMemoryDemo.main(DIrectBufferMemoryDemo.java:19)
unable to create new native thread
不能夠創(chuàng)建更多的新的線程了,也就是說(shuō)創(chuàng)建線程的上限達(dá)到了
在高并發(fā)場(chǎng)景的時(shí)候,會(huì)應(yīng)用到
高并發(fā)請(qǐng)求服務(wù)器時(shí),經(jīng)常會(huì)出現(xiàn)如下異常java.lang.OutOfMemoryError:unable to create new native thread,準(zhǔn)確說(shuō)該native thread異常與對(duì)應(yīng)的平臺(tái)有關(guān)
導(dǎo)致原因:
應(yīng)用創(chuàng)建了太多線程,一個(gè)應(yīng)用進(jìn)程創(chuàng)建多個(gè)線程,超過(guò)系統(tǒng)承載極限
服務(wù)器并不允許你的應(yīng)用程序創(chuàng)建這么多線程,linux系統(tǒng)默認(rèn)運(yùn)行單個(gè)進(jìn)程可以創(chuàng)建的線程為1024個(gè),如果應(yīng)用創(chuàng)建超過(guò)這個(gè)數(shù)量,就會(huì)報(bào) java.lang.OutOfMemoryError:unable to create new native thread
解決方法:
想辦法降低你應(yīng)用程序創(chuàng)建線程的數(shù)量,分析應(yīng)用是否真的需要?jiǎng)?chuàng)建這么多線程,如果不是,改代碼將線程數(shù)降到最低
對(duì)于有的應(yīng)用,確實(shí)需要?jiǎng)?chuàng)建很多線程,遠(yuǎn)超過(guò)linux系統(tǒng)默認(rèn)1024個(gè)線程限制,可以通過(guò)修改linux服務(wù)器配置,擴(kuò)大linux默認(rèn)限制
/**
* 無(wú)法創(chuàng)建更多的線程
*/
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("************** i = " + i);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
這個(gè)時(shí)候,就會(huì)出現(xiàn)下列的錯(cuò)誤,線程數(shù)大概在 900多個(gè)
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
如何查看線程數(shù)
ulimit -u
Metaspace
元空間內(nèi)存不足,Matespace元空間應(yīng)用的是本地內(nèi)存
-XX:MetaspaceSize 的初始化大小為20M
元空間是什么
元空間就是我們的方法區(qū),存放的是類模板,類信息,常量池等
Metaspace是方法區(qū)HotSpot中的實(shí)現(xiàn),它與持久代最大的區(qū)別在于:Metaspace并不在虛擬內(nèi)存中,而是使用本地內(nèi)存,也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存儲(chǔ)在叫做Matespace的native memory
永久代(java8后背元空間Metaspace取代了)存放了以下信息:
虛擬機(jī)加載的類信息
常量池
靜態(tài)變量
即時(shí)編譯后的代碼
模擬Metaspace空間溢出,我們不斷生成類 往元空間里灌輸,類占據(jù)的空間總會(huì)超過(guò)Metaspace指定的空間大小
代碼
在模擬異常生成時(shí)候,因?yàn)槌跏蓟脑臻g為20M,因此我們使用JVM參數(shù)調(diào)整元空間的大小,為了更好的效果
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
代碼如下:
/**
* 元空間溢出
*
*/
public class MetaspaceOutOfMemoryDemo {
// 靜態(tài)類
static class OOMTest {
}
public static void main(final String[] args) {
// 模擬計(jì)數(shù)多少次以后發(fā)生異常
int i =0;
try {
while (true) {
i++;
// 使用Spring的動(dòng)態(tài)字節(jié)碼技術(shù)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
}
} catch (Exception e) {
System.out.println("發(fā)生異常的次數(shù):" + i);
e.printStackTrace();
} finally {
}
}
}
會(huì)出現(xiàn)以下錯(cuò)誤:
發(fā)生異常的次數(shù): 201
java.lang.OutOfMemoryError:Metaspace
注意
在JDK1.7之前:永久代是方法區(qū)的實(shí)現(xiàn),存放了運(yùn)行時(shí)常量池、字符串常量池和靜態(tài)變量等。
在JDK1.7:永久代是方法區(qū)的實(shí)現(xiàn),將字符串常量池和靜態(tài)變量等移出至堆內(nèi)存。運(yùn)行時(shí)常量池等剩下的還再永久代(方法區(qū))
在JDK1.8及以后:永久代被元空間替代,相當(dāng)于元空間實(shí)現(xiàn)方法區(qū),此時(shí)字符串常量池和靜態(tài)變量還在堆,運(yùn)行時(shí)常量池還在方法區(qū)(元空間),元空間使用的是直接內(nèi)存。
-XX:MetaspaceSize=N//設(shè)置Metaspace的初始(和最小大?。?-XX:MaxMetaspaceSize=N//設(shè)置Metaspace的最大大小 與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創(chuàng)建,虛擬機(jī)會(huì)耗盡所有可用的系統(tǒng)內(nèi)存。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
Java實(shí)現(xiàn)學(xué)生成績(jī)輸出到磁盤(pán)文件的方法詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)將學(xué)生成績(jī)輸出到磁盤(pán)文件的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-11-11
@JsonFormat處理LocalDateTime失效的問(wèn)題
這篇文章主要介紹了關(guān)于@JsonFormat處理LocalDateTime失效的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Java的Spring框架中AOP項(xiàng)目的一般配置和部署教程
這篇文章主要介紹了Java的Spring框架中AOP項(xiàng)目的一般配置和部署教程,AOP面向方面編程的項(xiàng)目部署結(jié)構(gòu)都比較類似,因而也被看作是Spring的一種設(shè)計(jì)模式使用,需要的朋友可以參考下2016-04-04
學(xué)習(xí)Java之如何正確地向上轉(zhuǎn)型與向下轉(zhuǎn)型
面向?qū)ο蟮牡谌齻€(gè)特征是多態(tài),實(shí)現(xiàn)多態(tài)有三個(gè)必要條件:繼承、方法重寫(xiě)和向上轉(zhuǎn)型,在學(xué)習(xí)多態(tài)之前,我們還要先學(xué)習(xí)Java的類型轉(zhuǎn)換,本篇文章就來(lái)帶大家認(rèn)識(shí)什么是類型轉(zhuǎn)換,看看類型轉(zhuǎn)換都有哪幾種情況,以及如何避免類型轉(zhuǎn)換時(shí)出現(xiàn)異常2023-05-05
解析Java中所有錯(cuò)誤和異常的父類java.lang.Throwable
這篇文章主要介紹了Java中所有錯(cuò)誤和異常的父類java.lang.Throwable,文章中簡(jiǎn)單地分析了其源碼,說(shuō)明在代碼注釋中,需要的朋友可以參考下2016-03-03

