HotSpot 虛擬機(jī)對象探秘
對象的內(nèi)存布局
在 HotSpot 虛擬機(jī)中,對象的內(nèi)存布局分為以下 3 塊區(qū)域:
對象頭(Header)
實(shí)例數(shù)據(jù)(Instance Data)
對齊填充(Padding)

對象頭
對象頭記錄了對象在運(yùn)行過程中所需要使用的一些數(shù)據(jù):
哈希碼
GC 分代年齡
鎖狀態(tài)標(biāo)志
線程持有的鎖
偏向線程 ID
偏向時(shí)間戳
對象頭可能包含類型指針,通過該指針能確定對象屬于哪個(gè)類。如果對象是一個(gè)數(shù)組,那么對象頭還會(huì)包括數(shù)組長度。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分就是成員變量的值,其中包括父類成員變量和本類成員變量。
對齊填充
用于確保對象的總長度為 8 字節(jié)的整數(shù)倍。
HotSpot VM 的自動(dòng)內(nèi)存管理系統(tǒng)要求對象的大小必須是 8 字節(jié)的整數(shù)倍。而對象頭部分正好是 8 字節(jié)的倍數(shù)(1 倍或 2 倍),因此,當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時(shí),就需要通過對齊填充來補(bǔ)全。
對齊填充并不是必然存在,也沒有特別的含義,它僅僅起著占位符的作用。
對象的創(chuàng)建過程

類加載檢查
虛擬機(jī)在解析.class
文件時(shí),若遇到一條 new 指令,首先它會(huì)去檢查常量池中是否有這個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用所代表的類是否已被加載、解析和初始化過。如果沒有,那么必須先執(zhí)行相應(yīng)的類加載過程。
為新生對象分配內(nèi)存
對象所需內(nèi)存的大小在類加載完成后便可完全確定,接下來從堆中劃分一塊對應(yīng)大小的內(nèi)存空間給新的對象。分配堆中內(nèi)存有兩種方式:
指針碰撞
如果 Java 堆中內(nèi)存絕對規(guī)整(說明采用的是“復(fù)制算法”或“標(biāo)記整理法”),空閑內(nèi)存和已使用內(nèi)存中間放著一個(gè)指針作為分界點(diǎn)指示器,那么分配內(nèi)存時(shí)只需要把指針向空閑內(nèi)存挪動(dòng)一段與對象大小一樣的距離,這種分配方式稱為“指針碰撞”。空閑列表
如果 Java 堆中內(nèi)存并不規(guī)整,已使用的內(nèi)存和空閑內(nèi)存交錯(cuò)(說明采用的是標(biāo)記-清除法,有碎片),此時(shí)沒法簡單進(jìn)行指針碰撞, VM 必須維護(hù)一個(gè)列表,記錄其中哪些內(nèi)存塊空閑可用。分配之時(shí)從空閑列表中找到一塊足夠大的內(nèi)存空間劃分給對象實(shí)例。這種方式稱為“空閑列表”。
初始化
分配完內(nèi)存后,為對象中的成員變量賦上初始值,設(shè)置對象頭信息,調(diào)用對象的構(gòu)造函數(shù)方法進(jìn)行初始化。
至此,整個(gè)對象的創(chuàng)建過程就完成了。
對象的訪問方式
所有對象的存儲(chǔ)空間都是在堆中分配的,但是這個(gè)對象的引用卻是在堆棧中分配的。也就是說在建立一個(gè)對象時(shí)兩個(gè)地方都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對象,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對象的指針(引用)而已。 那么根據(jù)引用存放的地址類型的不同,對象有不同的訪問方式。
句柄訪問方式
堆中需要有一塊叫做“句柄池”的內(nèi)存空間,句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
引用類型的變量存放的是該對象的句柄地址(reference)。訪問對象時(shí),首先需要通過引用類型的變量找到該對象的句柄,然后根據(jù)句柄中對象的地址找到對象。
直接指針訪問方式
引用類型的變量直接存放對象的地址,從而不需要句柄池,通過引用能夠直接訪問對象。但對象所在的內(nèi)存空間需要額外的策略存儲(chǔ)對象所屬的類信息的地址。
需要說明的是,HotSpot 采用第二種方式,即直接指針方式來訪問對象,只需要一次尋址操作,所以在性能上比句柄訪問方式快一倍。但像上面所說,它需要額外的策略來存儲(chǔ)對象在方法區(qū)中類信息的地址。