0. 前言
JVM筆記系列伞剑,以JDK1.7為基準分歇,主要以《深入理解Java虛擬機》(第二版)和《Java虛擬機規(guī)范(Java SE 7版)》 為參考俗他,主要包括下圖所示的五部分內(nèi)容:1.類加載脐区,2.內(nèi)存區(qū)域块促,3.垃圾回收佃牛,4.JVM參數(shù)淹辞,5.JVM監(jiān)控工具。
本人是Java程序員俘侠,重點關注這些有助于優(yōu)化開發(fā)桑涎、性能調(diào)優(yōu)、問題解決等這些和具體生產(chǎn)密切相關的部分兼贡;關于Class的文件結構攻冷、編譯、指令等部分遍希,可以閱讀上述書籍或其它材料等曼。
本文主要記錄JVM內(nèi)存區(qū)域結構的相關知識,本文的主要知識點如下:
1. JVM內(nèi)存區(qū)域結構
JVM定義了若干程序運行期使用到的數(shù)據(jù)區(qū),其中一些隨著JVM進程啟動而創(chuàng)建禁谦,隨著JVM退出而銷毀胁黑;另一些則是與線程一一對應,隨著線程的啟動和結束而建立和銷毀州泊。JVM的運行時數(shù)據(jù)區(qū)分為5個部分丧蘸,如下圖所示,分別是程序計數(shù)器遥皂、Java棧力喷、Native方法棧、堆演训、方法區(qū)弟孟。
1.1 程序計數(shù)器(Program Counter)
- 程序計數(shù)器占用非常小的內(nèi)存,指向下一條指令的地址样悟。
- 每個線程擁有一個程序計數(shù)器拂募。
- 程序計數(shù)器在線程創(chuàng)建時創(chuàng)建。
- 如果是Java方法窟她,程序計數(shù)器指向字節(jié)碼指令的地址陈症。
- 如果是Native方法,程序計數(shù)器值則為空(Undefined)震糖。
- 程序計數(shù)器不會出現(xiàn)OutOfMemoryError爬凑。
1.2 Java棧
- Java棧是線程私有的,生命周期和線程相同试伙。
- 棧是由一系列棧幀組成的。
- 每個棧幀用于存儲局部變量表于样、操作數(shù)棧疏叨、動態(tài)鏈接、方法出口等信息穿剖。
- 每一個方法被調(diào)用到執(zhí)行完成的過程蚤蔓,就是一個棧幀在JVM從入棧到出棧的過程。
JVM規(guī)范中描述糊余,Java椥阌郑可能會出現(xiàn)兩種異常。
- StackOverflowError:線程請求的棧深大于虛擬機所允許的深度(例如無限遞歸)贬芥。
- OutOfMemoryError:虛擬機椡抡蓿可以動態(tài)擴展,如果擴展無法申請足夠的內(nèi)存時蘸劈,就會報出昏苏。
1.3 Native方法棧
本地方法棧和Java棧是非常相似的,Java棧是為了執(zhí)行Java方法服務,本地方法棧是為了執(zhí)行Native方法使用贤惯。在HotSpot虛擬機中洼专,Java棧和本地方法棧合二為一。
1.4 堆(Heap)
- Java堆是JVM所管理的內(nèi)存中最大的一塊孵构,生命周期和JVM進程相同屁商。
- 用于存放對象實例,幾乎所有的對象都在Heap上颈墅。
- Java堆是所有線程共享的空間蜡镶。
從垃圾回收的角度來說,Java堆分為新生代和老生代精盅,其中新生代還分為Eden帽哑、From Survivor(S0)、To Survivor(S1)三部分叹俏,如下圖所示妻枕。
默認參數(shù)下,新生代:老生代 = 1:2粘驰,Eden:Survivor = 8:1屡谐。Java堆中最大可用內(nèi)存 = 老生代+ Eden + Survivor*1,即S0和S1永遠有一個處于閑置的狀態(tài)蝌数,GC的時JVM候會把其中一個Survivor中存活的對象復制到另一個Survivor中愕掏。
- Eden區(qū)是Java實例對象優(yōu)先分配的區(qū)域,如果Eden沒有足夠的空間顶伞,將會執(zhí)行一次Minor GC饵撑。
- 經(jīng)過Minor GC后,Eden+S0(或者S1)中還存活的對象將會轉移到S1中唆貌,然后S0會被清空滑潘。
- Survivor中放不下的、存活次數(shù)超過一定數(shù)目的對象锨咙,會被轉移到老年代(Old)空間语卤,大對象也可能會直接分配到老年代(Old)空間。
- 當老年代(Old)空間不夠時酪刀,將會發(fā)生Major GC粹舵。
- 如果垃圾回收后,仍然沒有足夠的空間骂倘,那么將會拋出OutOfMemoryError眼滤。
1.5 方法區(qū)
- 方法區(qū)是線程共享的空間,生命周期和JVM進程相同历涝。
- 方法區(qū)用于存儲類的信息柠偶、常量池情妖、字段和方法數(shù)據(jù)、字節(jié)碼內(nèi)容等诱担。
在我們常用的HotSpot虛擬機中毡证,JDK1.7之前,使用PermGen(永久代)來實現(xiàn)方法區(qū)蔫仙;在JDK1.8中完全移除了PermGen料睛,改用Metaspace(元空間)來實現(xiàn)方法區(qū)。
其實摇邦,移除PermGen的工作從JDK1.7就開始了恤煞,符號引用(Symbols)、字面量(interned strings)施籍、類的靜態(tài)變量(class statics)在1.7中都轉移到了Heap中居扒,這大大減少了PermGen拋出OutOfMemoryError的機會。
Metaspace使用的是本地內(nèi)存丑慎,而非JVM內(nèi)存喜喂;因此Metaspace的大小限制,受限于物理內(nèi)存的的限制竿裂;當然它是可以通過參數(shù)-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 來指定的玉吁。
方法區(qū)的空間不夠用了,將會拋出OutOfMemoryError腻异。
關于方法區(qū)进副,運行時常量池特別值得一提,運行時常量池中的常量悔常,基本來源于各個class文件中的常量池影斑;程序運行時,除非手動向常量池中添加常量(比如調(diào)用String.intern方法)机打,否則jvm不會自動添加常量到常量池矫户。
1.6 直接內(nèi)存(Direct Memory)
直接內(nèi)存并不是JVM運行時數(shù)據(jù)區(qū)的一部分,屬于堆外(off-heap)內(nèi)存姐帚,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域。JDK1.4新增了NIO包障涯,引入了一種基于Channel和Buffer的IO方式罐旗,可以使用Native方法直接分配堆外內(nèi)存,然后通過存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作唯蝶。
//見 java.nio.ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// native方法九秀,見sun.misc.Unsafe類
public native long allocateMemory(long var1);
使用堆外內(nèi)存,可以擴展使用更大的內(nèi)存空間粘我,理論上能減少GC的暫停時間鼓蜒,還可以在進程間共享(MappedByteBuffer和FileChannel)痹换。
Direct Memory默認的大小是等同于JVM最大堆,我們可以通過-XX:MaxDirectMemorySize參數(shù)來控制其大小都弹。
如果直接內(nèi)存空間不夠用了娇豫,將會拋出OutOfMemoryError。
2. 對象的創(chuàng)建和訪問過程
2.1 對象的創(chuàng)建過程
類加載檢測畅厢。當new對象的時候冯痢,將會檢查能否在常量池中定位到一個類的符號引用,并檢查這個類是否被加載框杜、解析和初始化浦楣,如果沒有,則執(zhí)行相應的類加載過程咪辱。
類加載檢查通過后振劳,JVM將會為新生的對象分配內(nèi)存。如果Java堆內(nèi)存是規(guī)整的油狂,內(nèi)存分配采用“指針碰撞”方式历恐;如果內(nèi)存不是規(guī)整的,則采用“空閑列表”的方式选调。Java堆內(nèi)存是否規(guī)整夹供,取決于使用的垃圾回收器是否帶有壓縮整理的功能。因此仁堪,在使用Serial哮洽、ParNew等帶Compact過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞弦聂,而使用CMS這種基于Mark-Sweep算法的收集器時鸟辅,通常采用空閑列表。給對象分配內(nèi)線的過程莺葫,是指針移動的過程匪凉,它不是線程安全的,需要同步捺檬;為了解決這個問題再层,JVM給每個線程在Java堆中預先分配一塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)堡纬,這樣以來聂受,只有緩沖區(qū)用完了,重新分配時才需要同步操作烤镐。
對象內(nèi)存分配完畢之后蛋济,JVM把分配的內(nèi)存空間都初始化為零值。
JVM對對象做必要的設置炮叶。例如對象是哪個類的實例碗旅、如何找到類的元數(shù)據(jù)渡处、對象的哈希碼、對象的GC分代年齡等祟辟,這些信息存放在對象頭(Object Header)中医瘫。
至此,在JVM看來對象創(chuàng)建完成川尖;接下來執(zhí)行<init>方法登下,把對象按照程序員的意愿初始化,形成一個真正可用的對象叮喳。
2.2 對象的內(nèi)存布局
對象在堆中的布局分為三個區(qū)域:對象頭被芳,實例數(shù)據(jù),對齊填充馍悟。
對象頭 包括兩個部分畔濒,第一部分是“Mark Word”,用于存儲對象自身的運行時數(shù)據(jù)锣咒,包括HashCode侵状、GC分代年齡、鎖狀態(tài)標志毅整、線程持有的鎖趣兄、偏向ID、偏向時間戳等悼嫉;第二部分是類型指針艇潭,指向存放指向方法區(qū)的類數(shù)據(jù),即JVM通過這個指針來確定對象是哪個類的實例戏蔑。
實例數(shù)據(jù) 存放類的屬性蹋凝,包括父類的屬性信息。相同寬度的字段(例如long和double都是8字節(jié))分配在一起总棵。
對齊填充 這是虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍鳍寂,如果實例數(shù)據(jù)部分不是8字節(jié)的整數(shù)倍,那么就需要對齊填充來補齊情龄,除此之外迄汛,并無它意。
2.3 對象的訪問定位
引用存放在Java棧上骤视,數(shù)據(jù)類型為reference鞍爱;對象存放在Java堆中,引用是如何指向對象實例呢尚胞?
目前主流的訪問方式有兩種硬霍,1.使用句柄帜慢;2.使用直接指針笼裳。
如果使用句柄訪問唯卖,那么Java堆中將會分出一塊內(nèi)存作為句柄池,reference中存儲的就是對象的句柄地址躬柬,句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)的具體地址拜轨。句柄的好處在于,當對象被移動時(垃圾回收時發(fā)生)允青,只會改變句柄中的實例數(shù)據(jù)指針橄碾,reference本身不需要修改。
如果使用直接指針訪問颠锉,reference引用直接指向堆中的對象實例法牲,對象實例的對象頭存放對象類型指針,這種方式的好處在于琼掠,減少了一次指針定位的開銷拒垃,訪問速度更快。
HotSpot虛擬機中使用的是直接指針訪問的方式瓷蛙。
(完)