对象的内存布局 / Object o = new Object()
占用了多少内存?
(以64位虚拟机为前提)
首先我们要知道对象在内存中的布局:
三部分: 对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头(Header)
- 存储对象自身的运行时数据(Mark Word)—— 8字节
如哈希码(只在调用计算方法后才被加载)、GC分代年龄、锁状态标志、线程持有的锁等。
Mark Word被设计成一个动态定义的数据结构,以便在绩效的空间内存内存储尽量多的数据,根据对象的状态服用自己的存储空间。 - 类型指针 —— 默认8字节,开启压缩指针
-XX:+UserCompressedClassPointers
后占4字节
即对象指向它的类型元数据的指针。通过这个指针来确定该对象是哪个类的实例。 - 数组长度(仅用于数组对象)—— 4字节
有关压缩指针可以参考 JVM优化之压缩普通对象指针(CompressedOops),简单来说就是为了改善64位虚拟机造成的性能损耗所采取的优化措施。
主要有两个 一个是针对于类型指针的压缩指针 -XX:+UserCompressedClassPointers
,还有针对于普通对象的指针 -XX:-UseCompressedOops
。开启前都是8字节,开启后压缩至4字节。
实例数据
对象真正存储的有效信息,及我们在程序代码里面所定义的各种类型的字段内容。
对齐填充
HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说任何对象的大小都必须是8字节的倍数,如果不满足,则需要使用对齐填充来补全。
《深入Java虚拟机》中提到的对象头已经被精心设计成正好是8字节的整数倍,这是基于32位虚拟机或未开启压缩指针的64位虚拟机的前提条件下。在开启压缩指针的64位虚拟机中,对象头应该为12字节。
实例分析
接下来我们通过jol包来查看 o
对象的内存布局
import org.openjdk.jol.info.ClassLayout;
public class JOLTest {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
输出结果如下:
默认开启压缩指针的情况下:
o
对象占用了16个字节
- MarkWord 8字节
- ClassPointer 4字节
- 实例数据部分,因为是空对象所以为0
- 4 + 8 = 12 不是8的整数倍,由于下个对象需要进行对齐而造成了4字节的损失
关闭压缩指针的情况下 -XX:-UseCompressedClassPointers
:
o
对象占用了16个字节
- MarkWord 8字节
- ClassPointer 8字节
- 实例数据部分,因为是空对象所以为0
- 8 + 8 = 16 是8的整数倍
我们再来看一个自定义对象的内存布局,有User类如下(顺便复习各种数据类型长度(bushi
public class User {
private boolean aBoolean;
private byte aByte;
private short aShort;
private int anInt;
private long aLong;
private double aDouble;
private float aFloat;
private char aChar;
private String string;
private House house;
public User() {}
}
输出一个空user对象的内存布局
现在有两个问题
- 普通对象(如User中的String、House)占用字节数
- 三部分的分配方式
一个个解决
- 引用对象(如User中的String、House)占用字节数
首先是引用对象占用的字节数,结果中已经明确给出是4字节,但是这是在默认情况下,及默认开启压缩普通对象指针的情况下,若关闭,则将变为8字节。 - 三部分的分配方式
在《深入Java虚拟机》中有说到
HotSpot虚拟机默认的分配顺序为long/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
- 前面说过对齐填充是为了使对象的内存占用为8字节的倍数,而相同宽度的字段总是被分配到一起存放应该也是为了更好地分配内存。我们观察可以看到,对象头占用12字节,紧跟其后的便是4字节的int类型,构成16字节,若直接紧跟long的8字节,那么long会被分割,没这个必要。再往后,从long一直到byte,共占用26字节,此时,使用了2字节的占位符达到28字节,才与String的4字节分配到同一段。最后一行分配给剩下的oop——House。
特殊案例
数组
int[] ints = new int[15];
System.out.println(ClassLayout.parseInstance(ints).toPrintable());
输出如下
显然,对于数组对象,将会另外把数组长度信息存放至对象头中。
且对于数组的实例对象长度,如果是基本数据类型,直接计算 基本数组类型长度 * 数组长度;如果是普通对象,则为 4 * 数组长度,关闭压缩指针则为 8 * 数组长度。
总结
具体来说,对象的内存布局分为对象头、实例数据、对齐填充三部分。
针对于 32位虚拟机(/未开启压缩指针的64位虚拟机) 来说,首先是占用8(/16)字节的对象头。其中存储自身运行时数据的MarkWord占4(/8)字节,用于记录类型的指针ClassPointer占4(/8)字节,如果是数组,则在对象头中还存有4字节的数组长度。
其次是实例数据部分,该部分则根据默认分配策略以及实际情况(满足8字节的整数倍)来决定字段以及占位符的分配。
最后,若目前为止该对象的内存大小不为8字节的整数倍,则会因为下个对象的对齐而造成损失。
而对于 默认开启了压缩指针的64位虚拟机 来说,对象头因为类型指针的压缩而只占用12字节,因此在进行实例数据的内存分配时,会根据默认分配策略优先分配能够填充剩余4字节的字段,若没有则直接使用占位符。