一、先上答案
這個問題有坑,有兩種回答
第一種解釋:
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 word
和klass 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。符合我們上面的結論。