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。符合我們上面的結論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM