JVM探究之 —— HotSpot虛擬機對象探祕

本節以經常使用的虛擬機HotSpot和經常使用的內存區域Java堆爲例,深刻探討HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。程序員

1. 對象的建立

Java是一門面向對象的編程語言。在語言層面上,建立對象(例如克隆、反序列化)一般僅僅是一個new關鍵字而已,而在虛擬機中,對象(文中討論的對象限於普通Java對象,不包括數組和Class對象等)的建立又是怎樣一個過程呢?算法

對象建立過程以下:編程

1.1 類加載檢查

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。數組

1.2 分配內存

在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類加載完成後即可徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來。安全

內存分配方式有兩種:框架

  • 指針碰撞(Bump the Pointer):Java堆中內存是絕對規整的,全部用過的內存都放在一邊,空閒的內存放在另外一邊,中間放着一個指針做爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離。
  • 空閒列表(Free List):Java堆中的內存並非規整的,已使用的內存和空閒的內存相互交錯,沒法簡單地進行指針碰撞,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。

選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。所以,在使用Serial、ParNew等帶Compact過程的收集器時,系統採用的分配算法是指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器時,一般採用空閒列表。編程語言

在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:佈局

  • CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操做的原子性。
  • TLAB: 爲每個線程預先在 Eden 區分配一起內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大於 TLAB 中的剩餘內存或 TLAB 的內存已用盡時,再採用上述的 CAS 進行內存分配。

1.3 初始化零值

內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB,這一工做過程也能夠提早至TLAB分配時進行。這一步操做保證了對象的實例字段在Java代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。線程

1.4 設置對象頭

初始化零值後,虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。3d

1.5 執行init方法

在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象建立纔剛剛開始——<init>方法尚未執行,全部的字段都還爲零。因此,通常來講(由字節碼中是否跟隨invokespecial指令所決定),執行new指令以後會接着執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

2. 對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

2.1 對象頭

Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的自身運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。

2.2 實例數據

實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。

2.3 對齊填充

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。 由於 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

3. 對象的訪問定位

創建對象是爲了使用對象,咱們的Java程序須要經過棧上的reference數據來操做堆上的具體對象。因爲reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並無定義這個引用應該經過何種方式去定位、訪問堆中的對象的具體位置,因此對象訪問方式也是取決於虛擬機實現而定的。目前主流的訪問方式有使用句柄和直接指針兩種。

3.1 使用句柄

使用句柄訪問,Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

3.2 直接指針

使用直接指針訪問,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。

3.3 兩種對象訪問方式比較

這兩種對象訪問方式各有優點,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是一項很是可觀的執行成本。就虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各類語言和框架使用句柄來訪問的狀況也十分常見。

相關文章
相關標籤/搜索