深入理解JVM(③)HotSpot虛擬機對象探秘
前言
上篇文章介紹了Java虛擬機的運行時數(shù)據(jù)區(qū)域,大致明白了Java虛擬機內(nèi)存模型的概況,下面就基于實用優(yōu)先的原則,以最常用的虛擬機HotSpot和最常用的內(nèi)存區(qū)域Java堆為例,升入探討一下HotSpot虛擬機在Java堆中對象分配、布局和訪問的全過程。
對象的創(chuàng)建
Java是一門面向?qū)ο蟮木幊陶Z言,在Java程序的運行過程中每時每刻都有對象被創(chuàng)建出來,那么在虛擬機中,對象的創(chuàng)建是怎樣的一個過程呢?
當(dāng)Java虛擬機遇到一條字節(jié)碼new指令時,首先檢查這個指令的參數(shù)是否能定位到一個類的符號引用,然后檢查這個類是否已經(jīng)被加載、解析和初始化過。如果沒有,那么先執(zhí)行類型的加載過程。
為對象分配空間
在類加載檢查通過后,接下來虛擬機將為新生對象分配內(nèi)存。為對象分配空間的任務(wù)實際上便等同于把一塊確定大小的內(nèi)存塊兒從Java堆中劃分出來。
在解釋Java堆是如何為對象分配空間的時候,先解釋兩個虛擬機常用的分配空間方式。
- 指針碰撞
當(dāng)一塊兒內(nèi)存中的空間是絕對規(guī)整的時候,就是說,所有被使用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針,作為分界點的指示器,當(dāng)分配內(nèi)存是,就僅僅是把指針向空閑的方向挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump The Pointer)。 - 空閑列表
當(dāng)一塊兒內(nèi)存的空間不是規(guī)整的時候,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯在一起,那就沒辦法簡單地進(jìn)行指針碰撞了,虛擬機就必須維護(hù)一個列表,記錄哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
具體選擇哪種分配方式,是由Java堆中的內(nèi)存空間是否規(guī)則來決定的,而Java堆是否規(guī)整有由所采用的垃圾收集器是否帶有空間壓縮整理的能力決定。所以,當(dāng)使用Serial、ParNew等帶壓縮整理過的的收集器是,對象的分配方式是指針碰撞,而當(dāng)使用CMS這種基于清除算法的收集器是,理論上就只能采用較為復(fù)雜的空閑列表來分配內(nèi)存。
對象創(chuàng)建的線程安全
在對象創(chuàng)建的時候,除了如何劃分可用空間外,還有一個問題,那就是在分配內(nèi)存空間的時候如何保證線程安全。
解決這個問題有兩種方案:
- 一種是對分配內(nèi)存空間的動作進(jìn)行同步處理——實際上虛擬機是采用CAS配上失敗重試的方式保證更新操作的原子性;
- 另外一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allcation Buffer,TLAB),哪個線程要分配內(nèi)存,就在哪個線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩沖區(qū)時,才需要同步鎖定。
在保證了線程安全的為對象分配了內(nèi)存空間后,從虛擬機的視角來看,一個新的對象已經(jīng)產(chǎn)生了。
但是從Java程序的視角看來,對象創(chuàng)建才剛剛開始,構(gòu)造函數(shù),也就是Class文件中的< init >方法還沒有執(zhí)行,new 指令之后會執(zhí)行< init >方法,
按照程序員的意愿對對象進(jìn)行初始化,這樣一個真正可用的對象才算完全被構(gòu)造出來。
對象的內(nèi)存布局
在HotSpot虛擬機里,對象在堆內(nèi)存中的存儲布局可以劃分為三個部分:對象頭(Header)、實例數(shù)據(jù)(Insetance Data) 和對齊填充(Padding)。
對象頭
HotSpot虛擬機對象的對象頭包括兩類信息。
- 第一類是用于存儲對象自身運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志等。這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機中分別為32個比特和64個比特,稱為 “ Mark Word ” 。
- 對象頭的另外一部分是類型指針,即對象指向它的類型元數(shù)據(jù)的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因為需要通過數(shù)組的長度來確定對象的大小。
實例數(shù)據(jù)
實例數(shù)據(jù)是對象真正存儲的有效信息,即我們在程序代碼里面所定義的各種類型的字段內(nèi)容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響。
對齊填充
由于HotSpot虛擬機的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是任何對象的大小都必須是8字節(jié)的整數(shù)倍。對象頭部分已經(jīng)被精心設(shè)計成正好是8字節(jié)的倍數(shù)(1倍或2倍),因此如果對象實例數(shù)據(jù)部分沒喲對齊的話就需要通過對齊填充來補全。所以對齊填充為,并不是必然存在的一部分占位符。
對象的訪問定位
對象創(chuàng)建完成后就可以使用了,對象的定位是根據(jù)棧中的引用數(shù)據(jù),來確定對象在內(nèi)存中的位置的。那么如何通過引用數(shù)據(jù)定位到堆中的對象位置呢?
主流的訪問方式主要有使用句柄和直接指針兩種:
- 如果使用句柄訪問的話,Java堆中將可能會劃分出一塊內(nèi)存來作為句柄池,引用數(shù)據(jù)中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。
- 如果使用直接指針訪問的話,Java堆中對象的內(nèi)存布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,引用數(shù)據(jù)中存儲的直接就是對象地址,這樣訪問對象更快捷。
使用句柄,在對象被移動(垃圾收集時)時只會改變句柄中的數(shù)據(jù)指針,而直接指針節(jié)省了一次指針定位的時間開銷速度更快!
作者:紀(jì)莫
歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處。
限于本人水平,如果文章和代碼有表述不當(dāng)之處,還請不吝賜教。
歡迎掃描二維碼關(guān)注公眾號:Jimoer
文章會同步到公眾號上面,大家一起成長,共同提升技術(shù)能力。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。
您的鼓勵是博主的最大動力!


浙公網(wǎng)安備 33010602011771號