寫java時不管是我們自己new對象還是spring管理bean,儘管我們天天跟對象打交道,那麼對象的結構和記憶體佈局有多少人知道呢,這篇文章可帶你入門,瞭解java對象記憶體佈局。 本文涉及到JVM指針壓縮的知識點,不熟悉的小伙伴可以看前面寫過的一篇關於指針壓縮的文章。 [JVM之指針壓縮](http ...
寫java時不管是我們自己new對象還是spring管理bean,儘管我們天天跟對象打交道,那麼對象的結構和記憶體佈局有多少人知道呢,這篇文章可帶你入門,瞭解java對象記憶體佈局。
本文涉及到JVM指針壓縮的知識點,不熟悉的小伙伴可以看前面寫過的一篇關於指針壓縮的文章。
JVM之指針壓縮
首先說明,本文涉及的JDK版本是1.8,JVM虛擬機是64位的HotSpot實現為準。
java對象結構
關於java對象我們知道, 對象的實例是存在堆上的, 對象的元數據存在方法區(元空間)上,對象的引用保存在棧上的。那麼java對象的結構是什麼樣的呢,其實java對象由三部分構成。
- 對象頭
對象頭裡也有三部分構成。
- Markword
存儲對象的hashCode、垃圾回收對象的年齡以及鎖信息等。
- 類型指針
對象指向的類信息地址即元數據指針,比如User對象指針指向User.class的JVM記憶體地址。註意:jdk1.8以後元數據是存在Metaspace里的,jdk1.8之前是在方法區里
- 數組長度
只有對象是數組的情況下,才有這部分數據,若對象不是數組,則沒有這部分,不分配空間。
- 對象體
對象里的非靜態屬性占用的空間(包括父類的所有屬性,不區分修飾類型),不包括方法,註意:是非靜態屬性,屬於對象的屬性,靜態屬性是屬於類的不在對象上分配空間。如果屬性是基本數據類型,則直接存對象本身,如果是引用類型,則存的是對象的指針。
- 對齊填充
預設情況下,如果對象頭+對象體大小不是8位元組的倍數,則通過該部分進行補齊,比如對象頭+對象體大小隻有30位元組,則需要補齊到32位元組,這裡的對齊填充就是2位元組。預設情況下,JVM中對象是以8位元組對齊的,若對象頭加上對象體是8的倍數時,則不存在位元組對齊,否則會填充補齊到8的倍數。
對象結構如下圖所示。
通過圖中可以看出,數組對象只是在對象頭裡多了數組長度這一項,普通對象(非數組對象)沒有這項,也不分配記憶體空間。
對象結構及占用空間大小如下圖所示。
涉及指針壓縮的地方有兩個,一個是對象頭裡的類型指針,一個是對象體里的引用類型指針,這篇文章里有詳細的介紹:JVM之指針壓縮。
對象頭
對象頭包含三部分
- Markword:存儲對象自身運行時數據如hashcode、gc分代年齡及鎖信息等,64位系統總共占用8個位元組。
- 類型指針:對象指向類元數據地址的指針,jdk8預設開啟指針壓縮,64位系統占4個位元組
- 數組長度:若對象不是數組,則沒有該部分,不分配空間大小,若是數組,則為4個位元組長度
對象頭占用空間大小如下表所示。
Markword
存儲對象自身運行時數據如hashcode、gc分代年齡及鎖信息等,64位系統總共占用8個位元組,也就是64bit,64位的二進位0和1。
解釋如下:
- 對象的hashCode占31位,重寫類的hashCode方法返回int類型,只有在無鎖情況下,是在有調用的情況下會計算該值並寫到對象頭中,其他情況該值是空的。
- 分代年齡占4位,最大值也就是15,在GC中,當survivor區中對象複製一次,年齡加1,預設是到15之後會移動到老年代。
- 是否偏向鎖占1位,無鎖和偏向鎖的最後兩位都是01,使用這一位來標識區分是無鎖還是偏向鎖。
- 鎖標誌位占2位,鎖狀態標記位,同是否偏向鎖標誌位標識對象處於什麼鎖狀態。
- 偏向線程ID占54位,只有偏向鎖狀態才有,這個ID是操作系統層面的線程唯一id,跟java中的線程id是不一致的。
類型指針
類型指針指向類的元數據地址,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出來看到的對象大小不一樣,感興趣的可自行驗證。