如何判斷一個物件佔用多少位元組?
如何判斷一個物件佔用多少位元組?這是我之前遇到的一個面試題,在這裡分享一下。 要判斷一個物件佔用多少位元組,物件記憶體佈局是必須要了解的。
物件記憶體佈局
在HotSpot虛擬機器裡物件記憶體佈局分為3個部分:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)
物件頭
物件頭包括兩部分資訊:
- 第一部分用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC 分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。這部分資料的長度在32位和64位的虛擬機器(未開啟壓縮指標) 中分別為32個位元和64個位元, 官方稱它為“Mark Word”。
- 另外一部分是型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。
- 如果物件是一個 java 陣列,那麼在物件頭中還有一塊用於記錄陣列長度的資料。
Mark Word的32個位元儲存空間中的25個位元用於儲存物件雜湊碼, 4個位元用於儲存物件分代年齡,2個位元用於儲存鎖標誌位,1個位元固定為0。
HotSpot原始碼 markOop.hpp 註釋片段,描述了Mark Word的記憶體佈局:
例項資料
例項資料部分是物件真正儲存的有效資訊,即我們在程式程式碼裡面所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來。HotSpot虛擬機器預設的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上預設的分配策略中可以看到,相同寬度的欄位總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前。
對齊填充
對齊填充不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。 由於HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是任何物件的大小都必須是8位元組的整數倍。 物件頭部分已經被精心設計成正好是8位元組的倍數(1倍或者2倍),因此,如果物件例項資料部分沒有對齊的話, 就需要通過對齊填充來補全。
實踐出真知
下面來通過openjdk jol 來解讀物件佔用多少位元組。
JOL
JOL(Java Object Layout)是用於分析 JVM 中物件佈局方案的微型工具箱。這些工具大量使用 Unsafe、JVMTI 和 Serviceability Agent (SA) 來解碼實際的 物件佈局、佔用空間和引用。這使得 JOL 比其他依賴堆轉儲、規範假設等的工具更準確。
示例
- 建立一個maven專案,引入依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
- 新建一個類
@Data
public class BaseEntity {
private long id;
private double amount;
private int updateUserId;
private float f;
private char c;
private byte b;
private boolean bb;
private short ss;
private long[] list;
private String s;
private Long count;
}
- 測試
public class ObjectHeaderTest {
public static void main(String[] args) {
BaseEntity baseEntity = new BaseEntity();
String toPrintable = ClassLayout.parseInstance(baseEntity).toPrintable();
System.out.println(toPrintable);
}
}
列印結果:
site.sunlong.obj.BaseEntity object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c143
12 4 int BaseEntity.updateUserId 0
16 8 long BaseEntity.id 0
24 8 double BaseEntity.amount 0.0
32 4 float BaseEntity.f 0.0
36 2 char BaseEntity.c
38 2 short BaseEntity.ss 0
40 1 byte BaseEntity.b 0
41 1 boolean BaseEntity.bb false
42 2 (alignment/padding gap)
44 4 long[] BaseEntity.list null
48 4 java.lang.String BaseEntity.s null
52 4 java.lang.Long BaseEntity.count null
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
- 測試結果解讀 從測試結果可以看到:
- 資料第1行(object header: mark):這個是Mark Word佔用的位元組數,64位機器上佔用8個位元組,32位機器上佔用4個位元組。
- 資料第2行(object header: class) :型別指標,在開啟指標壓縮的情況下佔4個位元組,未開啟的情況下佔8個位元組,jdk1.6之後預設開啟指標壓縮。
- 資料第3-14行(除(alignment/padding gap)):BaseEntity物件屬性型別佔用位元組數,一共佔用39個位元組。
- 其他資料行:對齊填充2個位元組,由於Mark Word(8個位元組)+型別指標(4個位元組)+物件位元組數(42個位元組)=54個位元組,54不是8的倍數,所以要填充2個位元組湊夠8的倍數。如果位元組數之和剛好是8的倍數,則不需要對齊填充。
從結果也可以得到基本型別及普通物件指標位元組數表格:
型別 | 位元組數 |
---|---|
long/double | 8 |
int/float | 4 |
char/short | 2 |
byte/boolean | 1 |
oops(Ordinary Object Pointers) | 4 |
總結
通過我們上述測試得到的表格,在不依賴openjdk jol的情況下計算出一個物件屬性型別佔多少個位元組之後,開啟指標壓縮時,在物件屬性型別位元組數基礎上加12,未開啟指標壓縮時加16,這樣就能輕鬆的計算出一個物件佔多少個位元組了,從此面試不再怕。
參考資料:
《深入理解Java虛擬機器》