內(nèi)存溢出異常 OOM
我們知道:
- JVM的內(nèi)存模型
- 對(duì)象的創(chuàng)建和布局
開始面對(duì)最終Boss: OOM
我們的目標(biāo):
- 使用代碼驗(yàn)證Java內(nèi)存模型
- 在實(shí)際發(fā)生OOM時(shí)绷蹲,通過異常信息,瞬間判斷:
- 那個(gè)區(qū)域OOM
- 定位代碼
- 異常處理
堆OOM
什么情況下會(huì)發(fā)生堆OOM
- 不斷的在堆中創(chuàng)建對(duì)象
- 垃圾回收機(jī)制無(wú)法回收對(duì)象
不斷創(chuàng)建對(duì)象通過循環(huán)就可以了饲帅,但什么情況下垃圾回收機(jī)制無(wú)法回收對(duì)象呢
- GC通過GC Roots到對(duì)象之間的可達(dá)路徑來(lái)回收對(duì)象茂浮。
可作為GC Roots的對(duì)象有:
- 虛擬機(jī)棧引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中JNI引用的對(duì)象
- 這里使用第一種方式:虛擬機(jī)棧引用辙浑,即變量升略,存放循環(huán)創(chuàng)建的對(duì)象偎巢。
具體實(shí)現(xiàn):使用List集合蔼夜,循環(huán)添加測(cè)試對(duì)象。
集合中大量數(shù)據(jù)很常見呀压昼,也沒見到堆OOM
是的求冷,所以需要設(shè)置下虛擬機(jī)的內(nèi)存大小,和不可擴(kuò)展巢音。
JVM 參數(shù):
-Xmx20m : 表示設(shè)置虛擬機(jī)最大內(nèi)存20m
-Xms20m : 表示設(shè)置虛擬機(jī)最小內(nèi)存20m, 最大內(nèi)存=最小內(nèi)存遵倦,表示虛擬機(jī)不可擴(kuò)展。
我用的是STS, 這個(gè)在虛擬機(jī)參數(shù)在哪設(shè)置
- Run Configuration/Debug Configuration 中有VM參數(shù)這一項(xiàng)
- 設(shè)置Java -> Installed JREs選中使用的jdk/jre -> edit按鈕 -> 輸入VM參數(shù)
那報(bào)錯(cuò)OOM如何分析呢
一般日志只記錄報(bào)錯(cuò)堆棧官撼,無(wú)法確定某個(gè)類占用百分比或GC可達(dá)性分析等等梧躺。
分析OOM, 需要堆轉(zhuǎn)儲(chǔ)快照文件。即發(fā)生OOM之前的快照將堆棧中信息以文件信息保存下來(lái)
堆轉(zhuǎn)儲(chǔ)文件怎么設(shè)置傲绣?
設(shè)置JVM參數(shù)即可:-XX:+HeapDumpOnOutOfMemoryError
表示創(chuàng)建堆快照文件掠哥,在OOM異常發(fā)生時(shí)。
上代碼
代碼:
public class HeapOOM {
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
public class OOMObject {
}
報(bào)錯(cuò)異常:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7740.hprof ...
Heap dump file created [27970781 bytes in 0.088 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at jvm.com.oom.heap.HeapOOM.main(HeapOOM.java:12)
使用Memory Analysis 工具分析
可以看到主線程占用15.5M(97%)的空間
而class OOMObject一共有3,241,320個(gè)沒有釋放秃诵,占用了97%空間
所以問題就是這個(gè)對(duì)象無(wú)法釋放续搀,導(dǎo)致 OOM: Java heap space
虛擬機(jī)棧OOM
什么情況下會(huì)發(fā)生虛擬機(jī)棧OOM
虛擬機(jī)棧會(huì)有兩種情況:
- 棧空間不可擴(kuò)展菠净,當(dāng)前虛擬機(jī)棧深度 > 虛擬機(jī)規(guī)定的棧深度, 會(huì)拋出棧溢出錯(cuò)誤
- 椊希空間可擴(kuò)展,擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠內(nèi)存毅往,會(huì)拋出內(nèi)存溢出異常
測(cè)試1:
- 本地測(cè)試設(shè)置棧最大內(nèi)存參數(shù):-XSs10m
- 單線程使用死遞歸測(cè)試牵咙,并打印當(dāng)前棧深度。
- 測(cè)試結(jié)果:拋出的總是
棧溢出
異常攀唯,且棧深度在一定范圍內(nèi)變化 - 測(cè)試結(jié)論:可以看出洁桌,這是屬于第一種情況,當(dāng)前虛擬機(jī)棧深度 > 虛擬機(jī)規(guī)定的棧深度
新問題:
但棧深度在一定范圍內(nèi)變化侯嘀,是否表示每次虛擬機(jī)規(guī)定的棧深度不同另凌?
測(cè)試2:
修改棧最大的內(nèi)存參數(shù),數(shù)值縮小一半:-XSs5m
- 測(cè)試結(jié)果:還是拋出
棧溢出
異常戒幔,且棧深度在原來(lái)一半值左右變化 - 測(cè)試結(jié)論:也就是說吠谢,虛擬機(jī)棧深度并非虛擬機(jī)規(guī)定死的,而是通過虛擬機(jī)啟動(dòng)時(shí)當(dāng)前最大検ィ空間計(jì)算出來(lái)的囊卜。
新問題:
既然是通過最大棧空間計(jì)算的,如果擴(kuò)大每個(gè)棧幀大小栅组,椚钙埃空間在擴(kuò)展時(shí),可能無(wú)法申請(qǐng)到足夠內(nèi)存而拋出內(nèi)存溢出異常
測(cè)試3:
在遞歸方法添加多個(gè)局部變量玉掸,擴(kuò)大棧幀刃麸。
測(cè)試結(jié)果:還是拋出
棧溢出
異常,局部變量越多司浪,棧深度越小泊业。測(cè)試結(jié)論:虛擬機(jī)棧深度的計(jì)算,是在編譯期就計(jì)算好的啊易。
新問題:
編譯時(shí)怎么計(jì)算棧深度呢
我們知道:棧幀中的局部變量表在編譯時(shí)就知道大小吁伺,運(yùn)行時(shí)可以直接分配內(nèi)存
所以編譯期就知道棧幀大小,通過最大棧幀租谈,和椑貉伲空間最大值,可以知道棧深度最大多少割去。
新問題:
如何模擬椏呷矗空間內(nèi)存溢出?
這個(gè)棧深度是單線程情況下計(jì)算出來(lái)的呻逆,如果多線程情況下夸赫,線程越多,占用的椏С牵空間就越多茬腿,越可能發(fā)生棧空間內(nèi)存溢出異常宜雀。
但是測(cè)試案例無(wú)法模擬切平,因?yàn)閯?chuàng)建很多進(jìn)程在window環(huán)境下直接導(dǎo)致操作系統(tǒng)假死,Java的線程是映射到操作系統(tǒng)的內(nèi)核線程上州袒。
理論上: 多線程中為每個(gè)線程分配越大的內(nèi)存空間,越容易出現(xiàn)內(nèi)存溢出
原因:
- 操作系統(tǒng)分配給每個(gè)進(jìn)程的內(nèi)存是有限制的弓候,如32位windows是2G
- 虛擬機(jī)會(huì)設(shè)置Java堆內(nèi)存和方法區(qū)內(nèi)存最大值郎哭,即還剩下:2G - 最大堆內(nèi)存 - 最大方法區(qū)內(nèi)存
- 剩下內(nèi)存由虛擬機(jī)棧和本地方法棧瓜分,每個(gè)線程分配到的棧容量越大菇存,可建立的線程數(shù)量越少夸研。
建立新的線程時(shí),就容易發(fā)生內(nèi)存溢出異常依鸥。
以上結(jié)論待測(cè)試驗(yàn)證亥至!
方法區(qū)內(nèi)存溢出異常
方法區(qū)什么時(shí)候出現(xiàn)內(nèi)存溢出異常
方法區(qū)在不同jdk版本中實(shí)現(xiàn)不同
- jdk1.7之前,使用永生代實(shí)現(xiàn)
- jdk1.8之后,使用元空間實(shí)現(xiàn)
由于我現(xiàn)在使用的是jdk1.8, 無(wú)法模擬出永生代的內(nèi)存溢出姐扮,但原理基本一致絮供。
測(cè)試步驟:
設(shè)置虛擬機(jī)參數(shù),方法區(qū)空間最大值茶敏,且無(wú)法擴(kuò)展
永生代虛擬機(jī)參數(shù):-XX:PermSize=10M -XX:MaxPermSize=10M
元空間虛擬機(jī)參數(shù):-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M循環(huán)創(chuàng)建大量不同的類壤靶,直到內(nèi)存溢出。
使用CGlib字節(jié)碼動(dòng)態(tài)代理方式惊搏,可以在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建不同的類贮乳。
CGlib字節(jié)碼動(dòng)態(tài)代理在框架中經(jīng)常遇到,如Spring框架的AOP就是使用CGlib字節(jié)碼動(dòng)態(tài)代理實(shí)現(xiàn)的恬惯。
測(cè)試代碼:
public class PermOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
}
}
static class OOMObject {
}
}
測(cè)試結(jié)果:
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
測(cè)試結(jié)論:
可以看到向拆,當(dāng)元空間內(nèi)存不夠時(shí),大量的類就會(huì)造成元空間的內(nèi)存溢出
所以在Spring等框架運(yùn)用大量的CGlib字節(jié)碼動(dòng)態(tài)代理技術(shù)時(shí)酪耳,需要保證有大容量的元空間浓恳。
關(guān)于永久代有個(gè)字符串常量池的問題
String 有個(gè)intern()方法。
在jdk1.6中葡兑,會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代中奖蔓,返回的也是這個(gè)永久代中這個(gè)字符串的實(shí)例.
在jdk1.7之后,不會(huì)再?gòu)?fù)制字符串實(shí)例讹堤,只是在字符串常量池中記錄首次出現(xiàn)的實(shí)例引用吆鹤。
所以會(huì)有下面代碼中情況:
public class ContantsOOM {
public static void main(String[] args) {
// 指向字符串常量池中字符串
String str1 = "xuweizhen";
// str1在字符串常量池中已存在,str1.intern返回字符串常量值中首次出現(xiàn)的實(shí)例引用洲守,一致
System.out.println("1 :" + (str1.intern() == str1));
// 指向堆中字符串對(duì)象
String str2 = new StringBuilder("xuwei").append("zhen").toString();
// str2.intern()在字符串常量池中已存在疑务,不是首次出現(xiàn),所以返回的是str1的字符串常量池常量梗醇,與str2不一致知允。
System.out.println("2 :" + (str2.intern() == str2));
// 指向堆中字符串對(duì)象
String str22 = new StringBuilder("aaa").append("bbb").toString();
/**
* str22指向堆中字符串對(duì)象引用
* str22.intern方法判斷str22在字符串常量池中是否存在,str22不在字符串常量池中
* 將str22放入字符串常量池中叙谨,并返回該字符串常量池引用温鸽,所以一致。
*/
System.out.println("3 :" + (str22.intern() == str22));
// 指向堆中字符串對(duì)象
String str222 = new StringBuilder("a").append("aabbb").toString();
System.out.println("4 :" + (str222.intern() == str222));
// 指向堆中字符串對(duì)象
String str3 = new String("cccddd");
// str的new String()方法返回的是一個(gè)字符串副本手负,和原字符串引用并不一致
System.out.println(str3.intern() == str3);
}
}
直接內(nèi)存的內(nèi)存溢出異常
直接內(nèi)存在什么情況下出現(xiàn)內(nèi)存溢出異常
直接內(nèi)存容量通過參數(shù):-XX:MaxDirectMemorySize指定
可以通過Unsafe的allocateMemory方法分配直接內(nèi)存涤垫,但Unsafe類只有引導(dǎo)類加載器才會(huì)返回實(shí)例。這里無(wú)法實(shí)現(xiàn)竟终。
直接內(nèi)存測(cè)試待補(bǔ)充r疴!统捶!
想共同學(xué)習(xí)jvm的可以加我微信:1832162841榆芦,或者進(jìn)QQ群:982523529