首頁>技術>

一個物件在記憶體中究竟是怎樣進行佈局的,如何依據程式碼去確定物件佔據的大小,本文將進行粗略地探討。

物件在記憶體中的佈局,主要有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位元組。

13
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 利用STM32的外部中斷和定時器測量頻率