Java內(nèi)存區(qū)域與內(nèi)存溢出異常

引言

對(duì)于Java程序員來說,在虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制的幫助下,不再需要為每一個(gè)new操作去寫配對(duì)的delete/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題,由虛擬機(jī)管理內(nèi)存。但正是因?yàn)镴ava程序員把內(nèi)存控制權(quán)利交給了Java虛擬機(jī),一旦出現(xiàn)內(nèi)存泄漏和溢出方面,如果不了解虛擬機(jī)是如何使用內(nèi)存的,那么排查錯(cuò)誤將會(huì)非常困難。

2.2 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

2.2.1 程序計(jì)數(shù)器

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間。它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。

  • 為什么需要引入程序計(jì)數(shù)器?
    由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)時(shí)刻,一個(gè)處理器都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器不受影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。
    簡(jiǎn)而言之,就是存取當(dāng)前線程執(zhí)行的位置,以便下次執(zhí)行時(shí)正確恢復(fù)。

  • 注意:
    如果線程執(zhí)行的是一個(gè)Java方法,那么計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是native方法,那么這個(gè)計(jì)數(shù)器值為空。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域

2.2.2 Java虛擬機(jī)棧

Java虛擬機(jī)棧是線程私有的,其生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等。每個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)了一個(gè)棧幀在虛擬機(jī)中入棧到出棧的過程。

局部變量表

  • 概念:存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean,byte,char,short,int,float,long,double),對(duì)象引用和returnAddress類型(指向一條字節(jié)碼指令的地址);

  • 注意:
    a. 只有64位長(zhǎng)度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量空間,其余的類型只會(huì)占用1個(gè)局部變量空間。
    b. 局部變量表所需的內(nèi)存空間在編譯器完成分配,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。

2.2.3 本地方法棧

本地方法棧和虛擬機(jī)棧的作用是非常相似的,它們之間的區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法,而本地方法棧為虛擬機(jī)執(zhí)行native方法。

2.2.4 Java堆
  • Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。該內(nèi)存區(qū)域的唯一目的是存放對(duì)象實(shí)例。
  • 幾乎所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。
  • Java堆是垃圾收集器的主要區(qū)域。
  • Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。
  • 如果在堆中沒有足夠的內(nèi)存空間分配,并且堆也無法再擴(kuò)展時(shí),那么將會(huì)拋出OutOfMemoryError異常。
2.2.5 方法區(qū)

方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域。它用于存儲(chǔ)已被虛擬機(jī)加載的類信息,常量,靜態(tài)變量,即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

2.2.6 運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池為方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號(hào)引用。

2.3 HotSpot虛擬機(jī)對(duì)象探秘

2.3.1 對(duì)象的創(chuàng)建

在語言層面上,創(chuàng)建對(duì)象(例如克隆,反序列化)通常僅僅是一個(gè)new關(guān)鍵字而已,但在虛擬機(jī)中,對(duì)象的創(chuàng)建卻是一個(gè)復(fù)雜的過程。

對(duì)象創(chuàng)建的具體過程

  • a. 類加載檢查:虛擬機(jī)遇到一條new指令時(shí),首先會(huì)去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載,解析和初始化過。如果沒有,則必須先執(zhí)行相應(yīng)的操作。

  • b. 為對(duì)象分配內(nèi)存:為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。
    劃分方法有兩種:”指針碰撞“和”空閑列表“
    (1) 指針碰撞:假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有可用的內(nèi)存放在一邊,空閑的內(nèi)存放在一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那么分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離
    (2) 空閑列表:假設(shè)Java堆內(nèi)存并不是規(guī)整的,已使用的內(nèi)存空間和空閑的內(nèi)存空間是相互交錯(cuò)的,那么就無法通過指針碰撞來劃分了,虛擬機(jī)就必須維護(hù)一個(gè)表,記錄哪些內(nèi)存塊是可用的,在分配內(nèi)存時(shí)找到一塊足夠大的空間劃分給對(duì)象,并更新表中的記錄

  • 關(guān)于在并發(fā)情況下,使用指針碰撞分配空間存在的問題:
    我們知道“指針碰撞”方法是通過修改指針的位置來劃分空間,那么在并發(fā)的情況下,存在線程不安全性。可能出現(xiàn)正在給對(duì)象A分配空間,對(duì)象B又同時(shí)使用原來的指針位置來分配空間。
    解決方法:
    a. 對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理。
    b. 把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行。即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(簡(jiǎn)稱:TLAB)。哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。

  • c. 將分配的內(nèi)存空間都初始化為零值:如果使用的是TLAB,這一工作過程可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。

  • d. 對(duì)對(duì)象進(jìn)行必要的設(shè)置:例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例,如何找到類的元數(shù)據(jù)類型信息等,將這些信息存放在對(duì)象頭之中。

2.3.2 對(duì)象的內(nèi)存布局

對(duì)象在內(nèi)存中存儲(chǔ)的布局分為:對(duì)象頭,實(shí)例數(shù)據(jù)和對(duì)齊填充。

  • 對(duì)象頭包含兩部分信息:一部分是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),另一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。如果對(duì)象是一個(gè)Java數(shù)組,那么在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。

  • 實(shí)例數(shù)據(jù):對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。

  • 對(duì)齊填充:并不是必然存在的,僅僅起的是占位符的作用。因?yàn)?strong>HotSpot VM的自動(dòng)內(nèi)存管理要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。對(duì)象頭部分正好是8字節(jié)的倍數(shù),但當(dāng)對(duì)象實(shí)例部分沒有對(duì)齊時(shí),就需要通過對(duì)齊填充來補(bǔ)全。

2.3.3 對(duì)象的訪問定位

建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫]有定義這個(gè)引用通過何種方式去定位,訪問堆中對(duì)象的具體位置,所以對(duì)象的訪問方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主要的訪問方式為使用句柄和直接指針。

  • 使用句柄:Java堆劃分一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的是對(duì)象的句柄地址,而句柄包好了對(duì)象實(shí)例與類型數(shù)據(jù)各自的具體地址信息。
使用句柄.png
  • 使用直接指針:如果使用指針訪問,那么Java堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲(chǔ)的直接就是對(duì)象地址。
使用指針直接訪問.png
  • 兩者優(yōu)缺點(diǎn):
    使用句柄的最大好處是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不會(huì)移動(dòng)。
    使用直接指針訪問的最大好處是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象的訪問在Java中是非常頻繁的,因此這類開銷積少成多也是一項(xiàng)非常可觀的執(zhí)行成本。

2.4 實(shí)戰(zhàn):OutOfMemoryError異常

在Java虛擬機(jī)中,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OutOfMemoryError異常的可能。

2.4.1 Java堆溢出

Java堆用于存儲(chǔ)對(duì)象,只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象。

我們來看一個(gè)例子:

public class HeapOOM {
    static class OOMObject{
    }
    public static void main(String[] args){
        List<OOMObject> list = new ArrayList<>();
        //不斷創(chuàng)建對(duì)象,并添加到list中,以保留引用,防止被垃圾回收器回收
        while (true){
            list.add(new OOMObject());
        }
    }
}

在這里設(shè)置Java堆的大小為20MB,不可擴(kuò)展(將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動(dòng)擴(kuò)展),通過參數(shù)-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便事后進(jìn)行分析。


設(shè)置.png

輸出結(jié)果如下:


結(jié)果.png
2.4.2 虛擬機(jī)棧和本地方法棧溢出

關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:

  • 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
  • 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。

我們通過設(shè)置-Xss:128k來設(shè)定棧容量為128k,然后通過不斷調(diào)用方法來增加棧的深度。

public class JavaVMStackSOF {
    private int stackLength = 1;
    //不斷調(diào)用方法,從而增加棧深度
    public void stackLength(){
        stackLength ++;
        stackLength();
    }

    public static void main(String[] args){
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLength();
        } catch (Throwable e){
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }
    }
}

輸出結(jié)果:

棧異常.png
2.4.3 方法區(qū)和運(yùn)行時(shí)常量池溢出

由于常量池分配在永久代內(nèi),我們可以通過-XX:PermSize 和 -XX:MaxPermSize限制方法區(qū)大小,從而間接限制其中常量池的容量。

String.intern()是一個(gè)Native方法,它的作用是:如果字符串常量池中已經(jīng)包含了一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象;否則,將此String對(duì)象包含的字符添加到常量池中,并且返回此String對(duì)象的引用。

/**
 * VM Args: -XX:PermSize=20M -XX:MaxPermSize=20M
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args){
        //使用List保持著常量池引用,避免Full GC回收常量池行為
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true){
            list.add(String.valueOf(i ++).intern());
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評(píng)論 3 426
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,315評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評(píng)論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,189評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,388評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,613評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評(píng)論 1 293
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,112評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,334評(píng)論 2 377

推薦閱讀更多精彩內(nèi)容