本文討論的Java對象在內存中的大小指的是在堆(Heap)中的大小;未特殊說明,提到JVM的地方都指的是:Java HotSpot(TM) 64-Bit Server VM,版本:1.8.0_131
。html
Java中Object的組成:java
Object = Header + Primitive Fields + Reference Fields + Alignment & Padding`git
Header由兩部分組成:標記部分(Mark Word)和原始對象引用(Klass Pointer/Object Original Pointer)- mark word & klass pointer。github
word size
(64-bit JVM上是8 bytes,32-bit JVM上是4 bytes),包括了該對象的identity hash code和一些標記(好比鎖和年代信息)。UseCompressedOops
參數(jdk1.8 和jdk1.9默認是開啓的)。Primitive Fields && Reference Fields數組
類型 | 大小 |
---|---|
Object Reference | word size |
byte | 1 byte |
boolean | 1 byte |
char | 2 bytes |
short | 2 bytes |
int | 4 bytes |
float | 4 bytes |
double | 8 bytes |
long | 8 bytes |
對齊(Alignment)和補齊(Padding)jvm
任何對象都是以8 bytes的粒度來對齊的
。怎麼理解這句話呢?請看一個例子,new Object()
產生的對象的大小是多少呢?12 bytes的header,但對齊必須是8的倍數,還有4 bytes的alignment,因此對象的大小是16 bytes.ide
補齊,補齊的粒度是4 bytes
。wordpress
JVM分配內存空間一次最少分配8 bytes,對象中字段對齊的最小粒度爲4 bytes
。準備工做oop
本文使用Maven管理Jar包,源碼在這裏。佈局
pom.xml
中引入JOL(Java Object Layout, 使用實例 )依賴,用於展現對象在Heap中的分佈(layout):
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
第一個測試:
public static void main(String[] args) { System.out.println(VM.current().details()); }
執行後,會輸出:
# Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # WARNING | Compressed references base/shifts are guessed by the experiment! # WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE. # WARNING | Make sure to attach Serviceability Agent to get the reliable addresses. # Objects are 8 bytes aligned. // 以 8 bytes的粒度對齊 # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] // 分別對應[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]的大小 # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] // 數組中元素的大小,分別對應的是[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]
對象在Heap中的分佈遵循的規則:
重排序, JVM在Heap中給對象佈局時,會對field
進行重排序,以節省空間。
例-1
,對於類:
public class Reorder { private byte a; private int b; private boolean c; private float d; private Object e; public static void main(String[] args) { System.out.println(ClassLayout.parseClass(Reorder.class).toPrintable()); } }
若是沒有重排序,對象的分佈會是這個樣子的:
objectsize.Reorder object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 1 byte Reorder.a N/A 13 3 (alignment/padding gap) 16 4 int Reorder.b N/A 20 1 boolean Reorder.c N/A 21 3 (alignment/padding gap) 24 4 float Reorder.d N/A 28 2 char Reorder.e N/A 30 2 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 6 bytes internal + 2 bytes external = 8 bytes total
對象實例總大小:32 bytes,空間損失:8 bytes。
而實際是(運行main
方法會看到結果):
objectsize.Reorder object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Reorder.b N/A 16 4 float Reorder.d N/A 20 2 char Reorder.e N/A 22 1 byte Reorder.a N/A 23 1 boolean Reorder.c N/A Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
對象實例總大小:24 bytes,空間損失:0 bytes。
爲了不空間浪費,通常狀況下,field
分配的優先依次順序是:double > long > int > float > char > short > byte > boolean > object reference
。
注意到了沒,這裏有個基本的原則是:儘量先分配佔用空間大的類型
(除了object reference
)。這裏的儘量
有兩層含義:
在同等優先級狀況下,按這個順序分配。 例-2
:
public class Order { private int ignoreMeTentatively; private byte a; private boolean b; private char c; private short d; private int e; private float f; private double g; private long h; private Object i; public static void main(String[] args) { System.out.println(ClassLayout.parseClass(Order.class).toPrintable()); } }
這個類的實例在內存中分佈是:
objectsize.Reorder object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Reorder.b N/A 16 4 float Reorder.d N/A 20 2 char Reorder.e N/A 22 1 byte Reorder.a N/A 23 1 boolean Reorder.c N/A Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
請先忽略ignoreMeTentatively
字段,能夠驗證類型分配的順序。
在考慮到補齊(Padding)的狀況下,排在後面的類型有可能比排在前面的優先級更高。
回過頭來看例-1
和例-2
,會發現header
後的字一個field(offset 12)都是int
類型的。爲何呢?
這就是Alignment
和Padding
共同做用的結果。
JVM每次最少分配8 bytes的空間,而header
的大小是12。
也就是說,已經分配了16 bytes的空間了,若是嚴格按照前面說的那個順序,最早分配一個double
類型的field
,就須要在這以前先分配4 bytes的空間來補齊,也就這4 bytes的空間就白白浪費了。
這中狀況下,<=
Padding Size(4 bytes)的類型的優先級就高於大小>
Padding Size的類型了。
而在全部大小<=
Padding Size的類型中,int的優先級又是最高的,因此header
後的第一個field是int
。
爲了進一步理解,再來看個例子,例-3
:
public class Padding { private char a; private boolean b; private long c; private Object d; public static void main(String[] args) { System.out.println(ClassLayout.parseClass(Padding.class).toPrintable()); } }
這個類的實例在內存中分佈是:
objectsize.Padding object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 2 char Padding.a N/A 14 1 boolean Padding.b N/A 15 1 (alignment/padding gap) 16 8 long Padding.c N/A 24 4 java.lang.Object Padding.d N/A 28 4 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 1 bytes internal + 4 bytes external = 5 bytes total
能夠看到header
後的4個bytes空間分配狀況,在全部大小<=
Padding Size的類型中,char
的優先級最高,其次是boolean
,
這兩個加起來只有3 bytes(<Padding Size),而已經沒有1 byte大小的field了,因此只能分配1 byte的Padding。
接下來,JVM再分配一個8 bytes大小的空間,很明顯空間足夠的狀況下,long
的優先級最高,也正好用完這8 bytes的空間。
而後,JVM繼續分配一個8 bytes大小的空間,最後一個類型object reference
(這裏是Object
)了,在開啓UseCompressedOops
的狀況下,使用4 bytes的空間,還有4 bytes的空間只能用來對齊了。
子類和父類的field
永遠不會混合在一塊兒,而且父類的field
分配完以後纔會給子類的field
分配空間。
例-4
:
public class SuperA { long a; private int b; private float c; private char d; private short e; } public class SubA extends SuperA { private long d; public static void main(String[] args) { System.out.println(ClassLayout.parseClass(SubA.class).toPrintable()); } }
SubA
的實例在內存中的分佈是:
objectsize.SubA object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int SuperA.b N/A 16 8 long SuperA.a N/A 24 4 float SuperA.c N/A 28 2 char SuperA.d N/A 30 2 short SuperA.e N/A 32 8 long SubA.d N/A Instance size: 40 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
父類SuperA
中的field
所有分配完後,才分配子類SubA
的field
。
父類的的最後一個字段與子類的第一個字段以一個Padding Size(4 bytes)來對齊。
例-5
:
public class SuperB { private byte a; private int b; } public class SubB extends SuperB { private int a; private long b; public static void main(String[] args) { System.out.println(ClassLayout.parseClass(SubB.class).toPrintable()); } }
SubB
的實例在內存中分佈是:
objectsize.SubB object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int SuperB.b N/A 16 1 byte SuperB.a N/A 17 3 (alignment/padding gap) 20 4 int SubB.a N/A 24 8 long SubB.b N/A Instance size: 32 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
從offset 16的位置開始看,父類還有最後一個字段a
未分配,這時JVM分配一個8 bytes的空間,a
佔用1 byte,
還有7 bytes未使用,而這7 bytes空間沒有所有用於對齊,也就是說子類字段的分配並非從offset 24 開始的。
實際上只用了3 bytes空間來對齊(湊夠4 bytes的Padding Size),剩下的4 bytes分配給了子類的a
字段。
數組也是對象,但數組的header
中包含有一個int
類型的length值,又多佔了4 bytes的空間,因此數組的header
大小是16 bytes。
例-6
:
public class ArrayTest { public static void main(String[] args) { System.out.println(ClassLayout.parseInstance(new boolean[1]).toPrintable()); } }
長度爲1的boolean
數組的實例在內存的分佈是:
[Z object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 05 00 00 f8 (00000101 00000000 00000000 11111000) (-134217723) 12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 16 1 boolean [Z.<elements> N/A 17 7 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
能夠看到,header
佔用了16 bytes,一個boolean
元素佔用了1 bytes,剩餘7 bytes用於對齊。
參考資料