前言:剛開始接觸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ū)。