一個物件在記憶體中究竟是怎樣進行佈局的,如何依據程式碼去確定物件佔據的大小,本文將進行粗略地探討。
物件在記憶體中的佈局,主要有3個組成部分,包括物件頭,例項資料與對齊填充。確定物件的大小,也是從這3個組成部分的入手。
物件頭其中物件頭中又包括Mark Word與Klass Word。當該物件是一個數組時,物件頭還會增加一塊區域,用來儲存陣列的長度。以64位系統為例,物件頭儲存內容如下圖所示:
|---------------------------------------------------------------------------------------------------------------|| Object Header (128 bits) ||---------------------------------------------------------------------------------------------------------------|| Mark Word (64 bits) | Klass Word (64 bits) | |---------------------------------------------------------------------------------------------------------------|| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | lock:01 | OOP to metadata object | 無鎖|----------------------------------------------------------------------|---------|------------------------------|| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:01 | OOP to metadata object | 偏向鎖|----------------------------------------------------------------------|---------|------------------------------|| ptr_to_lock_record:62 | lock:00 | OOP to metadata object | 輕量鎖|----------------------------------------------------------------------|---------|------------------------------|| ptr_to_heavyweight_monitor:62 | lock:10 | OOP to metadata object | 重量鎖|----------------------------------------------------------------------|---------|------------------------------|| | lock:11 | OOP to metadata object | GC|---------------------------------------------------------------------------------------------------------------|
Mark Word該區域主要儲存hashcode、gc年齡、鎖標誌等。在32位系統上,Mark Word為32位,在64位系統上,為64位,即8個位元組。Mark Word在不同的鎖標誌(lock)下,結構也不盡相同。當然,lock相同時,比如lock=01,這時候需要藉助偏向鎖標記(biased_lock)來具體確定物件是否存在偏向鎖。
關於結合Mark Word講鎖的升級,可能要另外篇幅。不過,可以先看我的另外一篇文章Synchronized的最佳化,大致瞭解一下鎖的最佳化。
Klass Word該區域儲存物件的型別指標,該指標指向物件類元資料(類元資料都在方法區中,對方法區不熟悉的同學,可以先參考我的另外一篇文章靈性一問——為什麼用元空間替換永久代?),虛擬機器能夠透過這個指標,來確定該物件到底是哪個類的例項。在32位系統上,該區域佔用32位,在64位系統上,佔用64位,但是!當64位機器設定最大堆記憶體為32G以下時,將會預設開啟指標壓縮,將8位元組的指標壓縮為4位元組。當然也可以使用+UseCompressedOops直接開啟指標壓縮。
Array Length前面說過,如果物件是一個數組,那麼物件頭會增加一個額外的區域,用來記錄陣列的長度。在32位系統上,該區域佔用32位,在64位系統上,佔用64位,同樣的,如果開啟指標壓縮,則會壓縮到32位。
可以看得出來,一個非陣列的物件的物件頭佔用12個位元組,即Mark Word(8)+Klass Word(4)。
例項資料基本資料型別佔用的長度如下:
對於引用變數佔用的長度,同樣視系統位數而定。32位系統佔用4位元組,64位系統8位元組,開啟指標壓縮那就佔用4位元組。
例項資料部分只會存放物件的例項資料,並不會存放靜態資料。此外,子物件的例項資料部分會繼承父類所有例項資料,包括私有型別,這裡可以理解為子類擁有父類所有型別的成員變數,但在子類中無法直接訪問這些私有例項變數。
對齊填充這裡的對齊填充有兩方面:
(1)HotSpot虛擬機器規定物件的起始地址必須是8的整數倍,也就是要求物件的大小必須是8的整數倍。因此如果一個物件的物件頭+例項資料佔用的總記憶體沒有達到8的倍數時,會進行對齊填充,將總大小填充到最近的8的倍數上。
(2)欄位與欄位之前也需要對齊,欄位對齊的最小單位是4個位元組。
可以這樣理解,虛擬機器每次會為欄位發放一個最近的4倍數的一個盒子。比如,有個類的欄位有一個boolean和一個int,這時候先為boolean發放第一個大小為4位元組的盒子,將boolean放入其中,佔用1個位元組,浪費3個位元組,因為int佔用4個位元組,根本放不下,需要虛擬機器再分配一個大小為4的盒子。
虛擬機器不會按照欄位宣告的順序去給欄位分配盒子,而是會進行重排序,使得物盡其用。比如一個類有以下變數:char、int、boolean、byte。如果按照宣告順序去分配盒子的話,則需要為char分配一個盒子,浪費2個位元組。再為int分配一個盒子,這個盒子正好滿了,沒有浪費。接著為boolean分配一個盒子,浪費3個位元組。最後為byte分配一個盒子,又浪費3個位元組。
在進行重排序後,此時可以按照int(4)、char(2)+boolean(1)+byte(1)的順序,虛擬機器可以只分配2個盒子,大大減少記憶體浪費。但是引用型別的欄位必定在最後才分配。
例子例子位於64位機器上,其都開啟指標壓縮。
(1)例項化一個沒有任何屬性的空物件,那麼這個空物件佔用的記憶體大小為多少呢?
很簡單,物件頭佔用12位元組,還會利用4位元組進行填充,一共佔用16位元組。
(2)例項一個具有四個不同屬性的物件
class Test { public char charP; public int intP; public boolean booleanP; public byte byteP;}
這就是對齊填充部分舉的例子,物件頭佔用12位元組,例項資料佔用8位元組,此時一共20位元組,則物件填充需要佔用4位元組,一共佔用24位元組。
我們使用一個jol(Java Object Layout)工具來分析Test物件佔據的記憶體大小。只要在maven專案中引入這個依賴就好:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
然後在程式碼中這樣呼叫:
package com.yang;import org.openjdk.jol.info.ClassLayout;public class Main { public static void main(String[] args) { Test test = new Test(); System.out.println(ClassLayout.parseInstance(test).toPrintable()); }}
輸出如下:
看的出來,總大小確實為24位元組。
(3)例項化一個具有父類的子類
class Father { public boolean publicFlag; private boolean privateFlag; public static boolean staticFlag;}public class Test extends Father { public boolean publicFlag; private int b; protected double c; Long d;}
猜猜看,例項化一個Test物件後,這個物件佔據的記憶體大小是多少呢?
這裡可能會有幾個問題:
【1】子類的例項資料部分會排除掉父類的私有例項屬性privateFlag嗎?
【2】子類的例項資料部分會覆蓋掉父類的同名例項屬性嗎?
帶著這些疑問,我們直接使用jol檢視物件記憶體大小:
可以看到,子類物件中包含了父類所有的例項變數,且首先分配父類例項變數,再分配子類例項變數。物件頭還是佔用12位元組,父類例項變數佔用4位元組(包括2個位元組的欄位填充),子類例項變數佔用20位元組,物件填充佔用4位元組,一共佔用40位元組。