聊聊Java对象在内存中的大小
本文讨论的Java对象在内存中的大小指的是在堆(Heap)中的大小;未特殊说明,提到JVM的地方都指的是:Java HotSpot(TM) 64-Bit Server VM,版本:1.8.0_131
。
Java中Object的组成:
Object = Header + Primitive Fields + Reference Fields + Alignment & Padding`
Header由两部分组成:标记部分(Mark Word)和原始对象引用(Klass Pointer/Object Original Pointer)- mark word & klass pointer。
- 标记部分的大小是一个
word size
(64-bit JVM上是8 bytes,32-bit JVM上是4 bytes),包括了该对象的identity hash code和一些标记(比如锁和年代信息)。 - 原始对象引用在32-bit JVM上的大小是4 bytes,在64-bit JVM上可以是4 bytes,也可以是8 bytes,由JVM参数“是否压缩原始对象”决定,在HotSpot中是
UseCompressedClassPointers
参数(jdk1.8 和jdk1.9默认是开启的)。要开启UseCompressedClassPointers
须同时开启UseCompressedOops
;反之则没有这个约束。
- 标记部分的大小是一个
xu
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)
- 对齐,
任何对象都是以8 bytes的粒度来对齐的
。怎么理解这句话呢?请看一个例子,
new Object()
产生的对象的大小是多少呢?12 bytes的header,但对齐必须是8的倍数,还有4 bytes的alignment,所以对象的大小是16 bytes. 补齐,
补齐的粒度是4 bytes
。- 可以简单理解为,
JVM分配内存空间一次最少分配8 bytes,对象中字段对齐的最小粒度为4 bytes
。
- 对齐,
准备工作
本文使用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用于对齐。
参考资料
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。