寫java時不管是我們自己new物件還是spring管理bean,儘管我們天天跟物件打交道,那麼物件的結構和記憶體佈局有多少人知道呢,這篇文章可帶你入門,瞭解java物件記憶體佈局。
本文涉及到JVM指標壓縮的知識點,不熟悉的小夥伴可以看前面寫過的一篇關於指標壓縮的文章。
JVM之指標壓縮
首先說明,本文涉及的JDK版本是1.8,JVM虛擬機器器是64位元的HotSpot實現為準。
關於java物件我們知道, 物件的範例是存在堆上的, 物件的後設資料存在方法區(元空間)上,物件的參照儲存在棧上的。那麼java物件的結構是什麼樣的呢,其實java物件由三部分構成。
物件頭裡也有三部分構成。
儲存物件的hashCode、垃圾回收物件的年齡以及鎖資訊等。
物件指向的類資訊地址即後設資料指標,比如User物件指標指向User.class的JVM記憶體地址。注意:jdk1.8以後後設資料是存在Metaspace裡的,jdk1.8之前是在方法區裡
只有物件是陣列的情況下,才有這部分資料,若物件不是陣列,則沒有這部分,不分配空間。
物件裡的非靜態屬性佔用的空間(包括父類別的所有屬性,不區分修飾型別),不包括方法,注意:是非靜態屬性,屬於物件的屬性,靜態屬性是屬於類的不在物件上分配空間。如果屬性是基本資料型別,則直接存物件本身,如果是參照型別,則存的是物件的指標。
預設情況下,如果物件頭+物件體大小不是8位元組的倍數,則通過該部分進行補齊,比如物件頭+物件體大小隻有30位元組,則需要補齊到32位元組,這裡的對齊填充就是2位元組。預設情況下,JVM中物件是以8位元組對齊的,若物件頭加上物件體是8的倍數時,則不存在位元組對齊,否則會填充補齊到8的倍數。
物件結構如下圖所示。
通過圖中可以看出,陣列物件只是在物件頭裡多了陣列長度這一項,普通物件(非陣列物件)沒有這項,也不分配記憶體空間。
物件結構及佔用空間大小如下圖所示。
涉及指標壓縮的地方有兩個,一個是物件頭裡的型別指標,一個是物件體裡的參照型別指標,這篇文章裡有詳細的介紹:JVM之指標壓縮。
物件頭包含三部分
物件頭佔用空間大小如下表所示。
儲存物件自身執行時資料如hashcode、gc分代年齡及鎖資訊等,64位元系統總共佔用8個位元組,也就是64bit,64位元的二進位制0和1。
解釋如下:
型別指標指向類的後設資料地址,JVM通過這個指標確定物件是哪個類的範例。32位元的JVM佔32位元,4個位元組,64位元的JVM佔64位元,8個位元組,但是64位元的JVM預設會開啟指標壓縮,壓縮後也只佔4位元組。
64位元虛擬機器器中在堆記憶體小於32GB的情況下,UseCompressedOops是預設開啟的,該參數列示開啟指標壓縮,會將原來64位元的指標壓縮為32位元。
-XX:+UseCompressedClassPointers //開啟壓縮類指標
-XX:-UseCompressedClassPointers //關閉壓縮類指標
這個JVM引數依賴UseCompressedOops這個引數,UseCompressedOops開啟,UseCompressedClassPointers預設開啟,可手工關閉,UseCompressedOops關閉,UseCompressedClassPointers不管開啟還是關閉都不生效即不壓縮。
如果物件是普通物件非陣列物件,則沒有這部分,不佔用空間。
如果物件是一個陣列,則將陣列的長度存到物件頭裡,表示陣列的大小。
物件體裡放的是非靜態的屬性,也包括父類別的所有非靜態屬性(private修飾的也在這裡,不區分可見性修飾符),基本型別的屬性存放的是具體的值,參照型別及陣列型別存放的是參照指標。
虛擬機器器為了高效定址,採用8位元組對齊,所以物件大小不是8的倍數時,會補齊對應的位置,比如物件頭+物件體是32位元組時,則不需要對齊填充,物件頭+物件體是12位元組時,則需補齊4位元。
物件的大小跟指標壓縮是否開啟有關,可通過以下兩個引數控制。
UseCompressedClassPointers:壓縮類指標(開啟時類指標占4位元組,關閉時類指標占8位元組)
UseCompressedOops:壓縮普通物件指標(開啟時參照物件指標占4位元組,關閉時參照物件指標占8位元組)
這兩個引數預設是開啟的,即-XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手動設定,如下所示
-XX:+UseCompressedClassPointers //開啟壓縮類指標
-XX:-UseCompressedClassPointers //關閉壓縮類指標
-XX:+UseCompressedOops //開啟壓縮普通物件指標
-XX:-UseCompressedOops //關閉壓縮普通物件指標
32位元HotSpot VM是不支援UseCompressedOops引數的,只有64位元HotSpot VM才支援。
Oracle JDK從6 update 23開始在64位元系統上會預設開啟壓縮指標。
以下表格展示了物件中各部分所佔空間大小,單位:位元組。
型別 | 所屬部分 | 佔用空間大小(壓縮開啟) | 佔用空間大小(壓縮關閉) |
---|---|---|---|
Markwork | 物件頭 | 8 | 8 |
型別指標 | 物件頭 | 4 | 8 |
陣列長度 | 物件頭 | 4 | 4 |
byte | 物件體 | 1 | 1 |
boolean | 物件體 | 1 | 1 |
short | 物件體 | 2 | 2 |
char | 物件體 | 2 | 2 |
int | 物件體 | 4 | 4 |
float | 物件體 | 4 | 4 |
long | 物件體 | 8 | 8 |
double | 物件體 | 8 | 8 |
物件參照指標 | 物件體 | 4 | 8 |
對齊填充 | 對齊填充 | 物件頭+物件體是8的倍數?0 :8 -(物件頭+物件體)% 8 | 物件頭+物件體是8的倍數?0 :8 -(物件頭+物件體)% 8 |
物件大小計算公式
物件大小=物件頭 + 物件體(物件是陣列時,物件體的大小=參照指標占用空間大小*物件個數) + 對齊填充
64位元作業系統32G記憶體以下,預設開啟物件指標壓縮,物件頭是12位元組,關閉指標壓縮,物件頭是16位元組。記憶體超過32G時,則自動關閉指標壓縮,物件頭佔16位元組。
有了以上的理論知識,我們通過實際案例進行物件分析。
使用 JOL 工具分析 Java 物件大小
maven依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
常用類及方法
檢視物件內部資訊: ClassLayout.parseInstance(obj).toPrintable()
檢視物件外部資訊:GraphLayout.parseInstance(obj).toPrintable()
檢視物件佔用空間總大小:GraphLayout.parseInstance(obj).totalSize()
檢視類內部資訊:ClassLayout.parseClass(Object.class).toPrintable()
使用到的測試類:
@Setter
class Goods {
private byte b;
private char type;
private short age;
private int no;
private float weight;
private double price;
private long id;
private boolean flag;
private String goodsName;
private LocalDateTime produceTime;
private String[] tags;
public static String str;
public static int temp;
}
64位元JVM,堆記憶體小於32G的情況下,預設是開啟指標壓縮的。
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便麵");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
System.out.println(ClassLayout.parseInstance(goods).toPrintable());
}
計算物件大小:
先不看輸出結果,按上面的公式計算一下物件的大小:
物件頭:8位元組(Markword)+4位元組(類指標)=12位元組
物件體:1位元組(屬性b)+ 2位元組(屬性type)+ 2位元組(屬性age)+ 4位元組(屬性no)+ 4位元組(屬性weight)+ 8位元組(屬性price)+ 8位元組(屬性id)+ 1位元組(屬性flag) + 4位元組(屬性goodsName指標) + 4位元組(屬性produceTime指標) + 4位元組(屬性tags指標)= 42位元組(注意:靜態屬性不參與物件大小計算)
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (12 + 42) % 8 = 2位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 12位元組 + 42位元組 + 2位元組 = 56位元組。
執行看執行結果:
com.star95.study.jvm.Goods object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x2000c043
12 4 int Goods.no 123456
16 8 double Goods.price 1.5
24 8 long Goods.id 111
32 4 float Goods.weight 0.065
36 2 char Goods.type A
38 2 short Goods.age 10
40 1 byte Goods.b 1
41 1 boolean Goods.flag true
42 2 (alignment/padding gap)
44 4 java.lang.String Goods.goodsName (object)
48 4 java.time.LocalDateTime Goods.produceTime (object)
52 4 java.lang.String[] Goods.tags [(object), (object), (object)]
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
這裡有一個特殊的地方,列印輸出的屬性順序跟程式碼裡的順序不一致,這是因為JVM進行優化,也就是指令重排序,會根據屬性型別的大小、執行的先後順序對結果是否有影響、最小填充大小等因素計算出物件最小應占用的空間。
計算物件大小:
關閉壓縮指標,類指標和參照物件指標都佔8位元組,推算一下物件大小:
物件頭:8位元組(Markword)+8位元組(類指標)=16位元組
物件體:1位元組(屬性b)+ 2位元組(屬性type)+ 2位元組(屬性age)+ 4位元組(屬性no)+ 4位元組(屬性weight)+ 8位元組(屬性price)+ 8位元組(屬性id)+ 1位元組(屬性flag) + 8位元組(屬性goodsName指標) + 8位元組(屬性produceTime指標) + 8位元組(屬性tags指標)= 54位元組(注意:靜態屬性不參與物件大小計算)
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (16 + 54) % 8 = 2位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 16位元組 + 54位元組 + 2位元組 = 72位元組。
執行時增加JVM引數如下:
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便麵");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
System.out.println(ClassLayout.parseInstance(goods).toPrintable());
}
}
執行看執行結果:
com.star95.study.jvm.Goods object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x00000000175647b8
16 8 double Goods.price 1.5
24 8 long Goods.id 111
32 4 int Goods.no 123456
36 4 float Goods.weight 0.065
40 2 char Goods.type A
42 2 short Goods.age 10
44 1 byte Goods.b 1
45 1 boolean Goods.flag true
46 2 (alignment/padding gap)
48 8 java.lang.String Goods.goodsName (object)
56 8 java.time.LocalDateTime Goods.produceTime (object)
64 8 java.lang.String[] Goods.tags [(object), (object), (object)]
Instance size: 72 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
計算物件大小:
預設是開啟壓縮指標的,類指標和參照物件指標都佔4位元組,推算一下物件大小:
物件頭:8位元組(Markword)+ 4位元組(類指標) + 4位元組(陣列長度)= 16位元組
物件體:4位元組 * 3 = 12位元組
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (16位元組 + 12位元組)% 8= 4位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 16位元組 + 12位元組 + 4位元組 = 32位元組。
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便麵");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
Goods[] goodsArr = new Goods[3];
goodsArr[0] = goods;
System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
}
}
執行看執行結果:
[Lcom.star95.study.jvm.Goods; object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x2000c18d
12 4 (array length) 3
16 12 com.star95.study.jvm.Goods Goods;.<elements> N/A
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
計算物件大小:
關閉壓縮指標,類指標和參照物件指標都佔8位元組,推算一下物件大小:
物件頭:8位元組(Markword)+8位元組(類指標) + 4位元組(陣列長度)=20位元組
物件體:8位元組 * 3 = 24位元組
對齊填充:8 -(物件頭+物件體)% 8 = 8 - (20+ 24) % 8 = 4位元組
物件大小=物件頭 + 物件體 + 對齊填充 = 20位元組 + 24位元組 + 4位元組 = 48位元組。
執行時增加JVM引數如下:
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便麵");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
Goods[] goodsArr = new Goods[3];
goodsArr[0] = goods;
System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
}
}
執行看執行結果:
[Lcom.star95.study.jvm.Goods; object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x0000000017e04d70
16 4 (array length) 3
20 4 (alignment/padding gap)
24 24 com.star95.study.jvm.Goods Goods;.<elements> N/A
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
通過以上物件分析,我們看到在開啟壓縮指標的情況下,物件的大小會小很多,節省了記憶體空間。
通過以上的分析,基本已經把java物件的結構講清楚了,另外物件佔用記憶體空間大小也計算出來,有助於進行JVM調優分析,64位元的虛擬機器器記憶體在32G以下時預設是開啟壓縮指標的,超過32G自動關閉壓縮指標,主要目的都是為了提高定址效率。
另外,本文是通過JOL工具計算物件佔用空間的大小,不包括參照物件實際佔用的記憶體大小,因為計算時是按參照物件的指標占用空間大小計算的,可能跟其他工具計算的結果不一樣,具體跟工具的計算邏輯有關,比如跟JDK自帶的jvisualvm工具通過堆dump出來看到的物件大小不一樣,感興趣的可自行驗證。