JVM中各個(gè)區(qū)域內(nèi)存都是有限的界酒,在內(nèi)存不足的情況下,繼續(xù)分配新的內(nèi)存空間嘴秸,而不對(duì)老的內(nèi)存空間進(jìn)行回收釋放毁欣,測(cè)試就會(huì)產(chǎn)生內(nèi)存溢出庇谆,即大名鼎鼎的OOM
(Out Of Memory).
1. 產(chǎn)生OOM的區(qū)域
在JVM的五大區(qū)域(堆、Java虛擬機(jī)棧凭疮、本地方法棧饭耳、Metaspace、程序計(jì)數(shù)器)中执解,獨(dú)有程序計(jì)數(shù)器不會(huì)發(fā)生OOM寞肖,其他區(qū)域都會(huì)產(chǎn)生OOM,這是由于程序計(jì)數(shù)器中存儲(chǔ)的數(shù)據(jù)所占空間的大小不會(huì)隨程序的執(zhí)行而發(fā)生改變衰腌。所以新蟆,會(huì)導(dǎo)致OOM產(chǎn)生的區(qū)域是:
- 堆,存放新創(chuàng)建的對(duì)象
- Java虛擬機(jī)棧右蕊,存放棧幀琼稻、方法局部變量。
- 本地方法棧
- Metaspace饶囚, 主要用來(lái)存放類的字節(jié)碼信息
2. 解決OOM的思路
首先欣簇,需要確認(rèn)一點(diǎn)的是配置沒(méi)有劍走偏鋒,不存在某個(gè)區(qū)域給的內(nèi)存太小坯约,正常情況下就容易產(chǎn)生OOM熊咽。這點(diǎn)很容易做到,先看下服務(wù)器的JVM配置闹丐,然后再和推薦的配置進(jìn)行比較横殴。
服務(wù)器上的JVM配置,可以通過(guò)jinfo -flags 進(jìn)程號(hào)
來(lái)查看卿拴,具體可以參考:JVM參數(shù)簡(jiǎn)介
衫仑。而推薦的配置可以通過(guò)PerfMa社區(qū)提供的XXFox產(chǎn)品來(lái)獲取,XXFox有個(gè)根據(jù)機(jī)器配置一鍵生成JVM參數(shù)的功能堕花,示意圖如下:
如果JVM的內(nèi)存配置看起來(lái)是合理的文狱,那我們就需要定位到底是哪個(gè)對(duì)象生成太多導(dǎo)致了OOM。而想要知道哪個(gè)對(duì)象太多缘挽,我們就要獲取發(fā)生OOM時(shí)的內(nèi)存快照
文件(即dump
文件)瞄崇,只需要在JVM的啟動(dòng)參數(shù)中加入以下參數(shù):
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/oom
-XX:+HeapDumpOnOutOfMemoryError
表示在發(fā)生OOM的時(shí)候自動(dòng)將內(nèi)存快照dump出來(lái), -XX:HeapDumpPath=/usr/local/oom
參數(shù)指定dump文件存儲(chǔ)的路徑壕曼。
有了內(nèi)存快照之后苏研,我們就可以通過(guò)MAT之類的工具進(jìn)行分析。關(guān)于MAT的使用可以參考:JVM堆內(nèi)存分析工具-MAT
3. Java虛擬機(jī)棧的OOM
每個(gè)線程運(yùn)行時(shí)腮郊,都會(huì)創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧摹蘑,線程中運(yùn)行一個(gè)方法會(huì)創(chuàng)建一個(gè)棧幀,并將該棧幀入棧轧飞,方法執(zhí)行結(jié)束則棧幀出棧衅鹿。
一個(gè)線程的虛擬機(jī)棧內(nèi)存大小一般設(shè)置成1M, 假如程序不停的創(chuàng)建棧幀并入棧(即發(fā)生方法調(diào)用)撒踪,那么就有可能出現(xiàn)內(nèi)存溢出的情況。一般而言大渤,當(dāng)發(fā)生遞歸調(diào)用
糠涛,容易產(chǎn)生虛擬機(jī)棧的OOM。 以下程序模擬產(chǎn)生虛擬機(jī)棧的OOM
/**
* vm args:
* -XX:ThreadStackSize=1m
* @author zhangguicong
* @date 2019-12-23
*/
public class StackOomSample {
private static long counter = 0 ;
public static void main(String[] args) {
call();
}
private static void call () {
counter++;
System.out.println("第"+counter+"次調(diào)用call方法");
call();
}
}
解決辦法
對(duì)線程的棧內(nèi)存和棧幀來(lái)說(shuō)兼犯,它們是不存在GC,所以沒(méi)發(fā)通過(guò)查看GC日志來(lái)定位此類問(wèn)題集漾。另外切黔,內(nèi)存快照對(duì)此也無(wú)法提供幫助。
一般而言具篇,只要把所有的異常都寫(xiě)入本地日志文件中纬霞,那么當(dāng)系統(tǒng)發(fā)生問(wèn)題時(shí)我們直接去看日志文件中的異常信息就可以。棧內(nèi)存溢出的報(bào)錯(cuò)日志是:
Exception in thread "main" java.lang.StackOverflowError
4. Metaspace的OOM
Metaspace即JDK1.8之前常說(shuō)的方法區(qū)驱显,JVM運(yùn)行過(guò)程中會(huì)不停地往這個(gè)區(qū)域加載類诗芜。當(dāng)Metaspace區(qū)域快要滿了,會(huì)觸發(fā)一次Full GC埃疫, 而在每次Full GC的時(shí)候也會(huì)嘗試回收Metaspace區(qū)域
伏恐。Metaspace中class對(duì)象想要被回收,需要滿足以下條件:
- 所有由該class創(chuàng)建出來(lái)的對(duì)象都已經(jīng)被回收栓霜;
- class對(duì)象沒(méi)有引用執(zhí)行翠桦;
- 這個(gè)類的類加載器要先被回收。
因?yàn)橛辛艘陨蠗l件的限制胳蛮,所有class對(duì)象一般很難被回收掉销凑。
Metaspace一般極少發(fā)生OOM,如果真的發(fā)生了仅炊,請(qǐng)考慮以下兩種誘因斗幼。
- Metaspace內(nèi)存空間設(shè)置很小。一般不進(jìn)行配置的話抚垄,默認(rèn)只有幾十M蜕窿,對(duì)于大型項(xiàng)目,可能要加載的類很多呆馁,所以可能會(huì)不夠渠羞,一般而言,可以設(shè)置成512M智哀;
- 系統(tǒng)中使用動(dòng)態(tài)類加載技術(shù)(如次询,cglib)不當(dāng),導(dǎo)致動(dòng)態(tài)生產(chǎn)的類過(guò)多瓷叫。以下代碼模擬產(chǎn)生Metaspace的OOM
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* VM args:
* -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
* @author zhangguicong
* @date 2019-12-23
*/
public class MetaspaceOomSample {
public static void main(String[] args) {
dynamicCreateClass();
}
public static void dynamicCreateClass () {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Dog.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("eat")) {
System.out.println("play with dog before dog eat.");
}
return methodProxy.invokeSuper(o,objects);
}
});
Dog dog = ((Dog) enhancer.create());
dog.eat();
}
}
static class Dog {
public void eat () {
System.out.println("dog is eating bone");
}
}
}
5. 堆的OOM
對(duì)于虛擬機(jī)棧和Metaspace區(qū)域來(lái)說(shuō)屯吊,一般只要代碼上注意一些送巡,不太容易引發(fā)這兩塊區(qū)域的OOM。日常開(kāi)發(fā)中盒卸,更常見(jiàn)的是堆內(nèi)存中的OOM骗爆。
我們知道在堆內(nèi)存中,對(duì)象首先存放在新生代中蔽介,發(fā)生YoungGC之后可能進(jìn)入老年代摘投,老年內(nèi)存不足時(shí)再發(fā)生Full GC,但如果Full GC之后虹蓄,發(fā)現(xiàn)對(duì)象依然需要存活犀呼,無(wú)法將其內(nèi)存釋放掉,此時(shí)老年代無(wú)法再放入新的對(duì)象薇组,JVM只能選擇內(nèi)存溢出外臂。
發(fā)生堆內(nèi)存溢出的場(chǎng)景:
-
高并發(fā)
,系統(tǒng)承載請(qǐng)求量過(guò)大律胀,導(dǎo)致大量對(duì)象都是存活的宋光,無(wú)法放入新的對(duì)象; - 系統(tǒng)存在
內(nèi)存泄漏
的問(wèn)題炭菌,莫名其妙弄了很多的對(duì)象罪佳,沒(méi)有及時(shí)取消對(duì)他們的引用,結(jié)果對(duì)象都是存活的黑低,導(dǎo)致觸發(fā)GC無(wú)法回收菇民。
以下代碼模擬產(chǎn)生堆的OOM
/**
* vm args:
* -Xms10m -Xmx10m -XX:+PrintGCDetails -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
* @author zhangguicong
* @date 2019-12-23
*/
public class HeapOomSample {
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
while (true) {
personList.add(new Person("zs",23));
}
}
static class Person {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
}
MAT分析dump文件
我們運(yùn)行上面的示例代碼,不一會(huì)生成dump文件投储,使用MAT打開(kāi)這個(gè)dump文件第练,可以看到如下圖所示:
點(diǎn)擊Leak Suspects
,MAT給我們推測(cè)出來(lái)發(fā)生OOM的原因只有一個(gè):
我們看高亮部分的英文提示:main線程通過(guò)局部變量引用了96.98%的對(duì)象玛荞,且都被java.lang.Object[]
的一個(gè)實(shí)例對(duì)象引用著娇掏。此時(shí)我們并不能知道Object[]
中是什么東西,需要點(diǎn)擊 Details 》
勋眯,點(diǎn)擊之后婴梧,我們可以看到:
到這里,我們可以知道OOM產(chǎn)生的原因是堆內(nèi)存中產(chǎn)生了大量的Person對(duì)象導(dǎo)致的客蹋。
此外我們還可以點(diǎn)擊“See stacktrace”查看線程的執(zhí)行棧情況塞蹭,定位到可能發(fā)生OOM的代碼所在位置: