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

前言:剛開始接觸Java虛擬機(jī)的知識(shí)震肮,參考的是周志明的《深入理解Java虛擬機(jī)》這本書湿刽。一方面整理思路兽泣,同時(shí)也為了方便以后查閱怀读,所以整理了書中的內(nèi)容诉位。

? ? ? 本章首先介紹Java虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū)域,分為6個(gè)區(qū)域菜枷,主要從各個(gè)區(qū)域的作用苍糠、是否為線程共享、可能出現(xiàn)的異常進(jìn)行描述啤誊。然后介紹了對(duì)象的創(chuàng)建過程岳瞭、對(duì)象的內(nèi)存布局以及如何訪問對(duì)象,在對(duì)象的創(chuàng)建過程中要注意內(nèi)存分配的兩種方式(指針碰撞和空閑列表)蚊锹,在并發(fā)情況下如何做到線程安全(同步或者使用TLAB)瞳筏;對(duì)象的內(nèi)存布局包括對(duì)象頭,實(shí)例數(shù)據(jù)和對(duì)齊填充三個(gè)部分牡昆;對(duì)象的訪問有兩種方式姚炕,使用句柄訪問或者使用直接指針訪問。最后丢烘,是實(shí)戰(zhàn)部分柱宦,模擬了Java堆溢出、棧溢出和方法區(qū)與運(yùn)行時(shí)常量池的溢出铅协,并簡(jiǎn)要介紹了遇到這些情況如何分析和解決捷沸。

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

Java虛擬機(jī)所管理的內(nèi)存包括以下6個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在狐史,有些區(qū)域則依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀

1.程序計(jì)數(shù)器

1)如果線程正在執(zhí)行的是一個(gè)Java方法痒给,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;?如果正在執(zhí)行的是Native方法骏全,這個(gè)計(jì)數(shù)器的值為空

2)線程私有

3)唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域

2.Java虛擬機(jī)棧

1)描述的是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ù)類型、對(duì)象引用類型和returnAddress類型(指向了一條字節(jié)碼指令的地址)母怜;所需的內(nèi)存空間在編譯期間完成分配

2)線程私有

3)規(guī)定了兩種異常狀況:

如果線程請(qǐng)求的深度大于虛擬機(jī)所允許的深度余耽,將拋出StackOverflowError異常

如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展苹熏,而擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存碟贾,就會(huì)拋出OutOfMemoryError異常

3.本地方法棧

1)本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)币喧;虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)

Sun HotSpot虛擬機(jī)直接把虛擬機(jī)棧和本地方法棧合二為一

2)線程私有

3)規(guī)定了兩種異常狀況:StackOverflowError異常OutOfMemoryError異常

4.Java堆

1)此內(nèi)存唯一的目的是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存(不是所有的對(duì)象實(shí)例袱耽,因?yàn)殡S著JIT編譯器的發(fā)展與逃逸分析計(jì)數(shù)逐漸成熟杀餐,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化)

2)線程共享朱巨,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建

3)如果在堆中沒有內(nèi)存完成實(shí)例分配史翘,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常

4)Java堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊

Java堆是垃圾收集器管理的主要區(qū)域蔬崩,因此也被稱作GC堆(Garbage Collection Heap)

5.方法區(qū)

1)存儲(chǔ)已被虛擬機(jī)加載的類信息恶座、常量、靜態(tài)變量沥阳、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)

2)線程共享

3)當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)跨琳,將拋出OutOfMemoryError異常

4)這個(gè)區(qū)域的內(nèi)存回收的目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載

5)對(duì)于HotSpot虛擬機(jī),很多人把方法區(qū)稱為“永久代”桐罕,本質(zhì)上兩者不等價(jià)脉让,僅僅因?yàn)樗脑O(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)功炮,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存溅潜,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。但是薪伏,在JDK1.7中滚澜,已經(jīng)把原本放在永久代的字符串常量池移出

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

1)它是方法區(qū)的一部分

Class文件中有一項(xiàng)是常量池,用于存放編譯器生成的各種字面量和符號(hào)引用嫁怀,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放

常量池中主要存放兩大類常量:字面量和符號(hào)引用设捐。字面量比較接近于Java語言層面的常量的概念(如文本字符串、聲明為final的常量值等)塘淑;

符號(hào)引用則屬于編譯原理方面的概念萝招,包括了三類常量:類和接口的全限定名、字段的名稱和描述符存捺、方法的名稱和描述符

2)線程共享

3)當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常

4)并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池槐沼,運(yùn)行期間也可能將新的常量放入池中,如String類的intern()方法

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

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

1)虛擬機(jī)遇到一條new指令時(shí)捌治,首先將去檢查new指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用岗钩,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過肖油。如果沒有凹嘲,那么必須先執(zhí)行相應(yīng)的類加載過程

2)在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存

對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定构韵,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來周蹭。

i)根據(jù)Java堆是否規(guī)整,有兩種分配方式:

指針碰撞(Bump the Pointer)疲恢,假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的凶朗,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊显拳,中間放著一個(gè)指針作為分界點(diǎn)的指示器棚愤,那分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”杂数。

空閑列表(Free List)宛畦,假設(shè)Java堆中的內(nèi)存不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò)揍移,虛擬機(jī)就必須維護(hù)一個(gè)列表次和,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例那伐,并更新列表上的記錄踏施,這種分配方式稱為“空閑列表”。

而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定(如罕邀,Serial畅形、ParNew等帶整理過程,系統(tǒng)采用的分配算法是指針碰撞诉探;CMS基于標(biāo)記-清除日熬,通常采用空閑列表)

ii)并發(fā)情況下的分配

對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使僅僅修改一個(gè)指針?biāo)赶虻奈恢蒙隹瑁诓l(fā)情況下也并不是線程安全的竖席,解決這個(gè)問題有兩個(gè)方案:

對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理(虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性);

把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行阳液,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存怕敬,稱為本地線程分配緩存(Thread Local Allocation Buffer,TLAB)帘皿。哪個(gè)線程要分配內(nèi)存东跪,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí)鹰溜,才需要同步鎖定虽填。使用參數(shù)

-XX:+/-UseTLAB來設(shè)定虛擬機(jī)是否使用TLAB

3)內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭)

如果使用TLAB曹动,這一工作過程可以提前至TLAB分配時(shí)進(jìn)行斋日。

此操作保證實(shí)例字段在Java中不賦初值就可以直接使用

4)虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置,如這個(gè)對(duì)象是哪個(gè)類的實(shí)例墓陈、如何才能找到類的元數(shù)據(jù)信息恶守、對(duì)象的哈希碼值第献、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭之中

5)此時(shí)兔港,從虛擬機(jī)的視角看庸毫,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序員的視角看衫樊,對(duì)象創(chuàng)建才剛剛開始飒赃,方法還沒執(zhí)行,所有的字段都還為0.所以科侈,執(zhí)行new指令之后會(huì)接著執(zhí)行init方法载佳,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正的對(duì)象才算完全生產(chǎn)出來

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

在HotSpot虛擬機(jī)中臀栈,對(duì)象的內(nèi)存布局可以分為:對(duì)象頭蔫慧、實(shí)例數(shù)據(jù)、對(duì)齊填充

1)對(duì)象頭包括兩部分信息:

第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)挂脑,如哈希碼藕漱、GC分代年齡、鎖標(biāo)志狀態(tài)崭闲、線程持有的鎖肋联、偏向線程ID、偏向時(shí)間戳等刁俭。這部分?jǐn)?shù)據(jù)長(zhǎng)度在32位和64位的虛擬機(jī)中分別是32bit和64bit ? 官方稱它為Mark Word

第二部分是類型指針橄仍,即對(duì)象指向它的類元素?cái)?shù)據(jù)的指針,虛擬機(jī)通過指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例牍戚。(并不是所有的虛擬機(jī)都有類型指針)

如果對(duì)象是一個(gè)Java數(shù)組侮繁,在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。

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

這部分的存儲(chǔ)順序會(huì)收到虛擬機(jī)分配策略(相同寬度的字段總是被分配到一起)參數(shù)和字段在Java源碼中的定義順序的影響。

3)對(duì)齊填充

起著占位符的作用(對(duì)象的大小必須是8字節(jié)的整數(shù)倍)

3.對(duì)象的訪問定位

1)通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象

使用直接指針訪問(HotSpot虛擬機(jī))第晰,reference中存儲(chǔ)的直接就是對(duì)象地址锁孟。優(yōu)點(diǎn):速度快

2)使用句柄訪問,Java堆中會(huì)劃分出一塊內(nèi)存來作為句柄池茁瘦,reference中存儲(chǔ)的就是對(duì)象的句柄地址品抽,句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體信息。

優(yōu)勢(shì):reference中存儲(chǔ)的是穩(wěn)定的句柄地址甜熔,在對(duì)象被移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針圆恤,而reference本身不需要修改


三.實(shí)戰(zhàn)部分:OutOfMemoryError異常

1.Java堆溢出

Java堆是用來存儲(chǔ)對(duì)象實(shí)例的,如果不斷地創(chuàng)建對(duì)象腔稀,并且通過GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象盆昙,那么在對(duì)象數(shù)量達(dá)到堆容量的上限時(shí)就會(huì)溢出異常羽历。

1)如何模擬Java堆溢出呢?

設(shè)置JVM參數(shù):-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

限制堆的內(nèi)存為20M淡喜,在那么中通過死循環(huán)創(chuàng)建對(duì)象窄陡,就會(huì)出現(xiàn)OutOfMemoryError。

異常堆棧信息: java.lang.OutOfMemoryError: Java heap space

2)如何分析呢拆火?

上面JVM中的第三個(gè)參數(shù)是讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存轉(zhuǎn)儲(chǔ)快照,可以通過內(nèi)存映像分析工具(Eclipse Memory Analyzer)對(duì)Dump出來的轉(zhuǎn)儲(chǔ)快照進(jìn)行分析涂圆,分析是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow)们镜。

如果是內(nèi)存泄露,通過工具查看泄露對(duì)象到GC Roots的引用鏈润歉,掌握泄露對(duì)象的類型信息及GC Roots引用鏈的信息模狭,定位出泄露代碼的位置;

如果是內(nèi)存溢出踩衩,即內(nèi)存中的對(duì)象都必須存活著嚼鹉,那么檢查虛擬機(jī)的堆參數(shù)是否可以調(diào)大,并檢查代碼總是否存在某些對(duì)象生命周期過長(zhǎng)驱富、持有狀態(tài)時(shí)間過長(zhǎng)的情況锚赤,減少程序運(yùn)行期的內(nèi)存消耗。

2.虛擬機(jī)棧和本地方法棧溢出

對(duì)于HotSpot虛擬機(jī)褐鸥,棧容量由 -Xss參數(shù)設(shè)定线脚,會(huì)出現(xiàn)兩種異常。

1)如何模擬棧的溢出呢叫榕?

在單線程下浑侥,使用-Xss減少棧的容量或是定義大量的本地變量,增大此方法幀中本地變量表的長(zhǎng)度晰绎,都會(huì)拋出StackOverflowError寓落。

在多線程情況下,通過不斷建立線程的方式可以產(chǎn)生內(nèi)存溢出異常荞下。出現(xiàn)這種異常后伶选,如果是建立太多線程導(dǎo)致的內(nèi)存溢出,而且又不能減少線程數(shù)或更換64位虛擬機(jī)锄弱,可以通過減少最大堆和減少棧容量來換取更多的線程(Xmx:最大堆容量+MaxPermSize:最大方法區(qū)容量+棧容量)考蕾。

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

在JDK1.6中,String.intern方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代中会宪,返回的是永久代中這個(gè)字符串實(shí)例的引用肖卧;

在JDK1.7中,String.intern方法不再復(fù)制實(shí)例掸鹅,只是在常量池中記錄首次出現(xiàn)的實(shí)例的引用塞帐。

方法區(qū)的溢出拦赠,基本思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末葵姥,一起剝皮案震驚了整個(gè)濱河市荷鼠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榔幸,老刑警劉巖允乐,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異削咆,居然都是意外死亡牍疏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門拨齐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳞陨,“玉大人,你說我怎么就攤上這事瞻惋∠寐耍” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵歼狼,是天一觀的道長(zhǎng)掏导。 經(jīng)常有香客問我,道長(zhǎng)蹂匹,這世上最難降的妖魔是什么碘菜? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮限寞,結(jié)果婚禮上忍啸,老公的妹妹穿的比我還像新娘。我一直安慰自己履植,他們只是感情好计雌,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著玫霎,像睡著了一般凿滤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庶近,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天翁脆,我揣著相機(jī)與錄音,去河邊找鬼鼻种。 笑死反番,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播罢缸,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼篙贸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了枫疆?” 一聲冷哼從身側(cè)響起爵川,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎息楔,沒想到半個(gè)月后寝贡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡值依,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年兔甘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳞滨。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蟆淀,靈堂內(nèi)的尸體忽然破棺而出拯啦,到底是詐尸還是另有隱情,我是刑警寧澤熔任,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布褒链,位于F島的核電站,受9級(jí)特大地震影響疑苔,放射性物質(zhì)發(fā)生泄漏甫匹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一惦费、第九天 我趴在偏房一處隱蔽的房頂上張望兵迅。 院中可真熱鬧,春花似錦薪贫、人聲如沸恍箭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扯夭。三九已至,卻和暖如春鞍匾,著一層夾襖步出監(jiān)牢的瞬間交洗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工橡淑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留构拳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像隐圾,于是被迫代替她去往敵國(guó)和親伍掀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

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