Java虛擬機(jī)OOM

內(nèi)存溢出異常 OOM

我們知道:

  1. JVM的內(nèi)存模型
  2. 對(duì)象的創(chuàng)建和布局

開始面對(duì)最終Boss: OOM

我們的目標(biāo):

  1. 使用代碼驗(yàn)證Java內(nèi)存模型
  2. 在實(shí)際發(fā)生OOM時(shí)绷蹲,通過異常信息,瞬間判斷:
      1. 那個(gè)區(qū)域OOM
      1. 定位代碼
      1. 異常處理

堆OOM

什么情況下會(huì)發(fā)生堆OOM

  1. 不斷的在堆中創(chuàng)建對(duì)象
  2. 垃圾回收機(jī)制無(wú)法回收對(duì)象

不斷創(chuàng)建對(duì)象通過循環(huán)就可以了饲帅,但什么情況下垃圾回收機(jī)制無(wú)法回收對(duì)象呢

  1. GC通過GC Roots到對(duì)象之間的可達(dá)路徑來(lái)回收對(duì)象茂浮。
    可作為GC Roots的對(duì)象有:
    1. 虛擬機(jī)棧引用的對(duì)象
    1. 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
    1. 方法區(qū)中常量引用的對(duì)象
    1. 本地方法棧中JNI引用的對(duì)象
  1. 這里使用第一種方式:虛擬機(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è)置

  1. Run Configuration/Debug Configuration 中有VM參數(shù)這一項(xiàng)
  2. 設(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ì)有兩種情況:

  1. 棧空間不可擴(kuò)展菠净,當(dāng)前虛擬機(jī)棧深度 > 虛擬機(jī)規(guī)定的棧深度, 會(huì)拋出棧溢出錯(cuò)誤
  2. 椊希空間可擴(kuò)展,擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠內(nèi)存毅往,會(huì)拋出內(nèi)存溢出異常

測(cè)試1:

  1. 本地測(cè)試設(shè)置棧最大內(nèi)存參數(shù):-XSs10m
  2. 單線程使用死遞歸測(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)存溢出

原因:

  1. 操作系統(tǒng)分配給每個(gè)進(jìn)程的內(nèi)存是有限制的弓候,如32位windows是2G
  2. 虛擬機(jī)會(huì)設(shè)置Java堆內(nèi)存和方法區(qū)內(nèi)存最大值郎哭,即還剩下:2G - 最大堆內(nèi)存 - 最大方法區(qū)內(nèi)存
  3. 剩下內(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)不同

  1. jdk1.7之前,使用永生代實(shí)現(xiàn)
  2. jdk1.8之后,使用元空間實(shí)現(xiàn)

由于我現(xiàn)在使用的是jdk1.8, 無(wú)法模擬出永生代的內(nèi)存溢出姐扮,但原理基本一致絮供。

測(cè)試步驟:

  1. 設(shè)置虛擬機(jī)參數(shù),方法區(qū)空間最大值茶敏,且無(wú)法擴(kuò)展
    永生代虛擬機(jī)參數(shù):-XX:PermSize=10M -XX:MaxPermSize=10M
    元空間虛擬機(jī)參數(shù):-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M

  2. 循環(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末柄粹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匆绣,更是在濱河造成了極大的恐慌驻右,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,331評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犬绒,死亡現(xiàn)場(chǎng)離奇詭異旺入,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)凯力,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門茵瘾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人咐鹤,你說我怎么就攤上這事拗秘。” “怎么了祈惶?”我有些...
    開封第一講書人閱讀 167,755評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵雕旨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我捧请,道長(zhǎng)凡涩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,528評(píng)論 1 296
  • 正文 為了忘掉前任疹蛉,我火速辦了婚禮活箕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘可款。我一直安慰自己育韩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,526評(píng)論 6 397
  • 文/花漫 我一把揭開白布闺鲸。 她就那樣靜靜地躺著筋讨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪摸恍。 梳的紋絲不亂的頭發(fā)上悉罕,一...
    開封第一講書人閱讀 52,166評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音立镶,去河邊找鬼壁袄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛谜慌,可吹牛的內(nèi)容都是我干的然想。 我是一名探鬼主播莺奔,決...
    沈念sama閱讀 40,768評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼欣范,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼变泄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起恼琼,我...
    開封第一講書人閱讀 39,664評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妨蛹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后晴竞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛙卤,經(jīng)...
    沈念sama閱讀 46,205評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,290評(píng)論 3 340
  • 正文 我和宋清朗相戀三年噩死,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颤难。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,435評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡已维,死狀恐怖行嗤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垛耳,我是刑警寧澤栅屏,帶...
    沈念sama閱讀 36,126評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站堂鲜,受9級(jí)特大地震影響栈雳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缔莲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,804評(píng)論 3 333
  • 文/蒙蒙 一哥纫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酌予,春花似錦磺箕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至建椰,卻和暖如春雕欺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棉姐。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工屠列, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伞矩。 一個(gè)月前我還...
    沈念sama閱讀 48,818評(píng)論 3 376
  • 正文 我出身青樓笛洛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親乃坤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苛让,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,442評(píng)論 2 359