物件的記憶體佈局

yangxi_001發表於2013-12-05

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。

HotSpot虛擬機器的物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機器(未開啟壓縮指標)中分別為32bit和64bit,官方稱它為“Mark Word”。物件需要儲存的執行時資料很多,其實已經超出了32位、64位Bitmap結構所能記錄的限度,但是物件頭資訊是與物件自身定義的資料無關的額外儲存成本,考慮到虛擬機器的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲儘量多的資訊,它會根據物件的狀態複用自己的儲存空間。例如,在32位的HotSpot虛擬機器中,如果物件處於未被鎖定的狀態下,那麼Mark Word的32bit空間中的25bit用於儲存物件雜湊碼,4bit用於儲存物件分代年齡,2bit用於儲存鎖標誌位,1bit固定為0,而在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下物件的儲存內容見表2-1。

表2-1 HotSpot虛擬機器物件頭Mark Word
儲存內容 標 志 位 狀  態
物件雜湊碼、物件分代年齡 01 未鎖定
指向鎖記錄的指標 00 輕量級鎖定
指向重量級鎖的指標 10 膨脹(重量級鎖定)
空,不需要記錄資訊 11 GC標記
偏向執行緒ID、偏向時間戳、物件分代年齡 01 可偏向

物件頭的另外一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。並不是所有的虛擬機器實現都必須在物件資料上保留型別指標,換句話說,查詢物件的後設資料資訊並不一定要經過物件本身,這點將在2.3.3節討論。另外,如果物件是一個Java陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料,因為虛擬機器可以通過普通Java物件的後設資料資訊確定Java物件的大小,但是從陣列的後設資料中卻無法確定陣列的大小。

程式碼清單2-2為HotSpot虛擬機器markOop.cpp中的程式碼(註釋)片段,它描述了32bit下Mark Word的儲存狀態。

程式碼清單2-2 markOop.cpp片段
// Bit-format of an object header (most significant first, big endian layout below):
//  32 bits:
//  --------
//  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//  size:32 ------------------------------------------>| (CMS free block)
//  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

接下來的例項資料部分是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的儲存順序會受到虛擬機器分配策略引數(FieldsAllocationStyle)和欄位在Java原始碼中定義順序的影響。HotSpot虛擬機器預設的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的欄位總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前。如果CompactFields引數值為true(預設為true),那麼子類之中較窄的變數也可能會插入到父類變數的空隙之中。

第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。由於HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說,就是物件的大小必須是8位元組的整數倍。而物件頭部分正好是8位元組的倍數(1倍或者2倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

相關文章