0. 前言
JVM筆記系列,以JDK1.7為基準(zhǔn),主要以《深入理解Java虛擬機(jī)》(第二版)和《Java虛擬機(jī)規(guī)范(Java SE 7版)》 為參考,主要包括下圖所示的五部分內(nèi)容:1.類加載,2.內(nèi)存區(qū)域,3.垃圾回收,4.JVM參數(shù),5.JVM監(jiān)控工具。
本人是Java程序員,重點關(guān)注這些有助于優(yōu)化開發(fā)、性能調(diào)優(yōu)、問題解決等這些和具體生產(chǎn)密切相關(guān)的部分;關(guān)于Class的文件結(jié)構(gòu)、編譯、指令等部分,可以閱讀上述書籍或其它材料。
本文主要記錄JVM內(nèi)存區(qū)域結(jié)構(gòu)的相關(guān)知識,本文的主要知識點如下:
1. JVM內(nèi)存區(qū)域結(jié)構(gòu)
JVM定義了若干程序運(yùn)行期使用到的數(shù)據(jù)區(qū),其中一些隨著JVM進(jìn)程啟動而創(chuàng)建,隨著JVM退出而銷毀;另一些則是與線程一一對應(yīng),隨著線程的啟動和結(jié)束而建立和銷毀。JVM的運(yùn)行時數(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:線程請求的棧深大于虛擬機(jī)所允許的深度(例如無限遞歸)。
- OutOfMemoryError:虛擬機(jī)棧可以動態(tài)擴(kuò)展,如果擴(kuò)展無法申請足夠的內(nèi)存時,就會報出。
1.3 Native方法棧
本地方法棧和Java棧是非常相似的,Java棧是為了執(zhí)行Java方法服務(wù),本地方法棧是為了執(zhí)行Native方法使用。在HotSpot虛擬機(jī)中,Java棧和本地方法棧合二為一。
1.4 堆(Heap)
- Java堆是JVM所管理的內(nèi)存中最大的一塊,生命周期和JVM進(jìn)程相同。
- 用于存放對象實例,幾乎所有的對象都在Heap上。
- Java堆是所有線程共享的空間。
從垃圾回收的角度來說,Java堆分為新生代和老生代,其中新生代還分為Eden、From Survivor(S0)、To Survivor(S1)三部分,如下圖所示。
默認(rèn)參數(shù)下,新生代:老生代 = 1:2,Eden:Survivor = 8:1。Java堆中最大可用內(nèi)存 = 老生代+ Eden + Survivor*1,即S0和S1永遠(yuǎn)有一個處于閑置的狀態(tài),GC的時JVM候會把其中一個Survivor中存活的對象復(fù)制到另一個Survivor中。
- Eden區(qū)是Java實例對象優(yōu)先分配的區(qū)域,如果Eden沒有足夠的空間,將會執(zhí)行一次Minor GC。
- 經(jīng)過Minor GC后,Eden+S0(或者S1)中還存活的對象將會轉(zhuǎn)移到S1中,然后S0會被清空。
- Survivor中放不下的、存活次數(shù)超過一定數(shù)目的對象,會被轉(zhuǎn)移到老年代(Old)空間,大對象也可能會直接分配到老年代(Old)空間。
- 當(dāng)老年代(Old)空間不夠時,將會發(fā)生Major GC。
- 如果垃圾回收后,仍然沒有足夠的空間,那么將會拋出OutOfMemoryError。
1.5 方法區(qū)
- 方法區(qū)是線程共享的空間,生命周期和JVM進(jìn)程相同。
- 方法區(qū)用于存儲類的信息、常量池、字段和方法數(shù)據(jù)、字節(jié)碼內(nèi)容等。
在我們常用的HotSpot虛擬機(jī)中,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中都轉(zhuǎn)移到了Heap中,這大大減少了PermGen拋出OutOfMemoryError的機(jī)會。
Metaspace使用的是本地內(nèi)存,而非JVM內(nèi)存;因此Metaspace的大小限制,受限于物理內(nèi)存的的限制;當(dāng)然它是可以通過參數(shù)-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 來指定的。
方法區(qū)的空間不夠用了,將會拋出OutOfMemoryError。
關(guān)于方法區(qū),運(yùn)行時常量池特別值得一提,運(yùn)行時常量池中的常量,基本來源于各個class文件中的常量池;程序運(yùn)行時,除非手動向常量池中添加常量(比如調(diào)用String.intern方法),否則jvm不會自動添加常量到常量池。
1.6 直接內(nèi)存(Direct Memory)
直接內(nèi)存并不是JVM運(yùn)行時數(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)存的引用進(jìn)行操作。
//見 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)存,可以擴(kuò)展使用更大的內(nèi)存空間,理論上能減少GC的暫停時間,還可以在進(jìn)程間共享(MappedByteBuffer和FileChannel)。
Direct Memory默認(rèn)的大小是等同于JVM最大堆,我們可以通過-XX:MaxDirectMemorySize參數(shù)來控制其大小。
如果直接內(nèi)存空間不夠用了,將會拋出OutOfMemoryError。
2. 對象的創(chuàng)建和訪問過程
2.1 對象的創(chuàng)建過程
類加載檢測。當(dāng)new對象的時候,將會檢查能否在常量池中定位到一個類的符號引用,并檢查這個類是否被加載、解析和初始化,如果沒有,則執(zhí)行相應(yīng)的類加載過程。
類加載檢查通過后,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堆中預(yù)先分配一塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),這樣以來,只有緩沖區(qū)用完了,重新分配時才需要同步操作。
對象內(nèi)存分配完畢之后,JVM把分配的內(nèi)存空間都初始化為零值。
JVM對對象做必要的設(shè)置。例如對象是哪個類的實例、如何找到類的元數(shù)據(jù)、對象的哈希碼、對象的GC分代年齡等,這些信息存放在對象頭(Object Header)中。
至此,在JVM看來對象創(chuàng)建完成;接下來執(zhí)行<init>方法,把對象按照程序員的意愿初始化,形成一個真正可用的對象。
2.2 對象的內(nèi)存布局
對象在堆中的布局分為三個區(qū)域:對象頭,實例數(shù)據(jù),對齊填充。
對象頭 包括兩個部分,第一部分是“Mark Word”,用于存儲對象自身的運(yùn)行時數(shù)據(jù),包括HashCode、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向ID、偏向時間戳等;第二部分是類型指針,指向存放指向方法區(qū)的類數(shù)據(jù),即JVM通過這個指針來確定對象是哪個類的實例。
實例數(shù)據(jù) 存放類的屬性,包括父類的屬性信息。相同寬度的字段(例如long和double都是8字節(jié))分配在一起。
對齊填充 這是虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍,如果實例數(shù)據(jù)部分不是8字節(jié)的整數(shù)倍,那么就需要對齊填充來補(bǔ)齊,除此之外,并無它意。
2.3 對象的訪問定位
引用存放在Java棧上,數(shù)據(jù)類型為reference;對象存放在Java堆中,引用是如何指向?qū)ο髮嵗兀?/p>
目前主流的訪問方式有兩種,1.使用句柄;2.使用直接指針。
如果使用句柄訪問,那么Java堆中將會分出一塊內(nèi)存作為句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)的具體地址。句柄的好處在于,當(dāng)對象被移動時(垃圾回收時發(fā)生),只會改變句柄中的實例數(shù)據(jù)指針,reference本身不需要修改。
如果使用直接指針訪問,reference引用直接指向堆中的對象實例,對象實例的對象頭存放對象類型指針,這種方式的好處在于,減少了一次指針定位的開銷,訪問速度更快。
HotSpot虛擬機(jī)中使用的是直接指針訪問的方式。
(完)