Object o = new Object()佔多少個位元組?-物件的記憶體佈局

dijia478發表於2021-04-19

一、先上答案

這個問題有坑,有兩種回答

第一種解釋:

object例項物件,佔16個位元組。

第二種解釋:

Object o:普通物件指標(ordinary object pointer),佔4個位元組。
new Object():object例項物件,佔16個位元組。
所以一共佔:4+16=20個位元組。

第二種解釋就是在玩文字遊戲了,但還是要知道的。

二、這個答案適用於所有情況嗎

並不是,這個答案只適用於現在一般預設情況。

準確的說,只適用於Hotspot實現64位虛擬機器,預設開啟了壓縮類指標壓縮普通物件指標的情況下。

本文下述內容若無特殊說明,指的都是JDK8 Hotspot實現64位虛擬機器的未開啟壓縮的情況。

三、前置知識

在 JVM 中,Java物件儲存在堆中時,由以下三部分組成:

  • 物件頭(Object Header):包括關於堆物件的佈局、型別、GC狀態、同步狀態和標識雜湊碼的基本資訊。由兩個詞mark wordklass pointer組成,如果是陣列物件的話,還會有一個length field
    • mark word:通常是一組位域,用於儲存物件自身的執行時資料,如hashCode、GC分代年齡、鎖同步資訊等等。佔用64個位元,8個位元組。
    • klass pointer:類指標,是物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。佔用64個位元,8個位元組。開啟壓縮類指標後,佔用32個位元,4個位元組。
  • 例項資料(Instance Data):儲存了程式碼中定義的各種欄位的內容,包括從父類繼承下來的欄位和子類中定義的欄位。如果物件無屬性欄位,則這裡就不會有資料。根據欄位型別的不同佔不同的位元組,例如boolean型別佔1個位元組,int型別佔4個位元組等等。為了提高儲存空間的利用率,這部分資料的儲存順序會受到虛擬機器分配策略引數和欄位在Java原始碼中定義順序的影響。
  • 對齊填充(Padding):物件可以有對齊資料也可以沒有。預設情況下,Java虛擬機器堆中物件的起始地址需要對齊至8的整數倍。如果一個物件的物件頭和例項資料佔用的總大小不到8位元組的整數倍,則以此來填充物件大小至8位元組的整數倍。

為什麼要對齊填充?欄位記憶體對齊的其中一個原因,是讓欄位只出現在同一CPU的快取行中。如果欄位不是對齊的,那麼就有可能出現跨快取行的欄位。也就是說,該欄位的讀取可能需要替換兩個快取行,而該欄位的儲存也會同時汙染兩個快取行。這兩種情況對程式的執行效率而言都是不利的。其實對其填充的最終目的是為了計算機高效定址。

我看到網路上有些文章把mark word稱之為物件頭,把java物件的記憶體佈局分為4個部分mark word、klass pointer、instance data、padding,這很明顯是沒有看過官方文件的,說法並不嚴謹。關於物件頭,可以在Hotspot官方文件找到下面的描述:

四、詳細解釋

因為第二種解釋包含了第一種解釋,所以我們分析第二種解釋。

1.Object o

在Hotspot實現的64位虛擬機器中,原本情況下,它內部的一個引用,就應該佔64個位元,也就是8個位元組。什麼叫引用啊?上面那個變數小o,就叫引用,也叫普通物件指標(別說什麼java裡沒有指標,什麼引用和指標不一樣。我不想去爭論這個)。但是,在第二種解釋中我們說了,普通物件指標,佔4個位元組,怎麼又成8個位元組了,怎麼回事呢?

這是因為Hotspot實現的64位虛擬機器,預設會開啟壓縮普通物件指標,會把8個位元組的物件引用,壓縮成4個位元組。

Object o佔用大小分為兩種情況:

  • 未開啟壓縮物件指標

    8位元組

  • 開啟壓縮物件指標(預設是開啟的)

    4位元組

2.new Object()

同樣的,在Hotspot實現的64位虛擬機器中,原本情況下,類指標應該佔64個位元,也就是8個位元組。但因為Hotspot實現的64位虛擬機器,預設會開啟壓縮類指標(和壓縮物件指標不一樣),而類指標就在Klass Pointer中儲存著,所以會把Klass Pointer壓縮成4個位元組。

new Object()佔用大小分為兩種情況:

  • 未開啟壓縮類指標

    8位元組(Mark Word) + 8位元組(Klass Pointer) = 16位元組

  • 開啟壓縮類指標(預設是開啟的)

    8位元組(Mark Word) + 4位元組(Klass Pointer) + 4位元組(Padding) = 16位元組

五、驗證

光說不練假把式,實踐出真知,上面的只是理論,我們來實際驗證下,是不是真的是這樣。

1.驗證預設開啟壓縮

首先,我們來看下,JDK8 Hotspot實現64位虛擬機器,是不是會預設開啟壓縮類指標壓縮物件指標

win + R,輸入cmd,敲入下面的命令java -version,相信大家對這個命令很熟悉了,檢視java版本

接下來我們加個引數-XX:+PrintCommandLineFlags,這個引數讓JVM列印出那些已經被使用者或者JVM設定過的詳細的XX引數的名稱和值,注意看下面兩個引數

-XX:+UseCompressedClassPointers:使用壓縮類指標

-XX:+UseCompressedOops:使用壓縮普通物件指標

可以看到,這兩個配置是預設開啟的。

注意:32位HotSpot VM是不支援UseCompressedOops引數的,只有64位HotSpot VM才支援。

什麼是oop?

這引數後面的oop可不是物件導向程式設計Object Oriented Programming的意思,而是普通物件指標Ordinary Object Pointer。

啟用UseCompressedOops後,會壓縮的物件:

  • 每個Class的屬性指標(靜態成員變數);
  • 每個物件的屬性指標;
  • 普通物件陣列的每個元素指標。

當然,壓縮也不是所有的指標都會壓縮,對一些特殊型別的指標,JVM是不會優化的,例如指向PermGen的Class物件指標、本地變數、堆疊元素、入參、返回值和NULL指標不會被壓縮。

關於UseCompressedClassPointers和UseCompressedOops

這樣一看,好像UseCompressedOops對Object的記憶體佈局並沒有影響,其實不然,開啟UseCompressedOops,預設會開啟UseCompressedClassPointers,會壓縮klass pointer這部分的大小,由8位元組壓縮至4位元組,間接的提高記憶體的利用率。關閉UseCompressedOops預設會關閉UseCompressedClassPointers。

如果開啟UseCompressedClassPointers,根據上面的條件,結果跟只開啟UseCompressedOops一樣,會在記憶體中消耗20個位元組,o指標佔4個位元組,Object物件佔16個位元組。

如果關閉UseCompressedClassPointers,根據上面的條件,UseCompressedOops還是會開啟,會在記憶體中消耗20個位元組,o指標佔4個位元組,Object物件佔16個位元組。

如果開啟類指標壓縮,+UseCompressedClassPointers,並關閉普通物件指標壓縮,-UseCompressedOops,此時會報錯,UseCompressedClassPointers requires UseCompressedOops。因為UseCompressedClassPointers的開啟是依賴於UseCompressedOops的開啟。

2.驗證例項物件佈局大小

上面已經看到,JVM預設開啟了壓縮類指標壓縮普通物件指標,那麼在這個情況下,new Object()是否真的是8位元組(Mark Word) + 4位元組(Klass Pointer) + 4位元組(Padding) = 16位元組呢?

還好 openjdk 給我們提供了一個工具包,可以用來獲取物件的資訊和虛擬機器的資訊,我們只需引入 jol-core 依賴,如下:

<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.14</version>
</dependency>

jol-core 常用的三個方法:

  • ClassLayout.parseInstance(object).toPrintable():檢視物件內部資訊.
  • GraphLayout.parseInstance(object).toPrintable():檢視物件外部資訊,包括引用的物件.
  • GraphLayout.parseInstance(object).totalSize():檢視物件總大小.

簡單物件

為了簡單化,我們不用複雜的物件,自己建立一個類 Test01,先看無屬性欄位的時候

public class Test01 {

    public static void main(String[] args) {
        Test01 t = new Test01();
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
    }

}

通過 jol-core 的 api,我們將物件的內部資訊列印出來:

可以看到有 OFFSET、SIZE、TYPE DESCRIPTION、VALUE 這幾個名詞頭,它們的含義分別是

  • OFFSET:偏移地址,單位位元組;
  • SIZE:佔用的記憶體大小,單位為位元組;
  • TYPE DESCRIPTION:型別描述,其中object header為物件頭;
  • VALUE:對應記憶體中當前儲存的值,二進位制32位;

同時可以看到,t例項物件共佔據16Byte,object header佔據12Byte,其中mark word佔8Byte,klass pointer佔4Byte,padding佔4Byte。

如果我把壓縮普通物件指標的引數去掉呢?可以通過配置vm引數關閉壓縮類指標,-XX:-UseCompressedClassOops。我們再看看結果:

可以看到,物件頭所佔用的記憶體大小變為16Byte,其中mark word佔8Byte,klass pointer佔8Byte,無padding。

至此,已經證明了我們上面的結論是正確的。

有成員變數的物件

我們現在再給Test01類里加4個成員變數,開啟指標壓縮,看看它的佈局吧

public class Test01 {

    String a = "a";

    int b = 1;

    boolean c = false;

    char d = 'd';

    public static void main(String[] args) {
        Test01 t = new Test01();
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
    }

}

可以看到,物件大小變成了24Byte,其中mark word佔8Byte,klass pointer佔4Byte,int佔4Byte,char佔2Byte,boolean佔1Byte,padding佔1Byte,String型別的變數a佔4Byte,也驗證了我們上面說的“為了提高儲存空間的利用率,這部分資料的儲存順序會受到虛擬機器分配策略引數和欄位在Java原始碼中定義順序的影響”,可以看到記憶體中的佈局順序確實和我們定義的不一樣。

此時我再關閉兩個指標壓縮,再看看佈局變化:

可以看到,物件總大小變成了32Byte,和開啟壓縮指標相比,klass pointer大了4Byte,String型別的變數a大了4Byte。符合我們上面的結論。

相關文章