Object o = new Object()佔多少個字節?-對象的內存佈局

一、先上答案

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

第一種解釋:

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,此時會報警告,"Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops"。因爲UseCompressedClassPointers的開啓是依賴於UseCompressedOops的開啓。

總結下就是,開了UseCompressedOops,UseCompressedClassPointers可開可不開,默認會也被打開。關了UseCompressedOops,UseCompressedClassPointers開了會報警告,默認會也被關掉。這兩個配置,在不特意修改的情況下都是默認開啓的。

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:-UseCompressedClassPointers。我們再看看結果:

可以看到,t實例對象還是佔據16Byte,但object header所佔用的內存大小變爲16Byte,其中mark word佔8Byte,klass pointer佔8Byte,無padding。

klass pointer的大小從上面的4Byte,變成了8Byte,正是因爲沒有對它進行壓縮。同時也因爲對象大小已經達到16Byte,是8的整數倍,所以不再需要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。符合我們上面的結論。

參考:https://www.likecs.com/show-131497.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章