对象的内存布局
在HotSpot虚拟机中,对象在堆中的存储布局可以划分为三个部分
- 对象头(
Header)Mark Work- 类型指针
- 数组长度(如果对象是数组)
- 实例数据(
Instance Data) - 对齐填充(
Padding)
对象头
HotSpot虚拟机对象的对象头部分包括两类信息。
Mark Work
一类是用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态、线程持有的锁、偏向线程ID、偏向时间戳等。
这部分数据的长度在32位和64位的虚拟机中分别占用32和64个比特,官方称为Mark Work。
为了存储的空间效率,Mark Work被设计成一个有着动态定义的数据结构,以便在极小的空间存储尽量多的数据。
其存储情况如下:
| 存储内容 | 标志位 | 状态 |
|---|---|---|
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 重量级锁定(锁膨胀) |
| 空、不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 |
01 | 可偏向 |
32位虚拟机占用4个字节,64位虚拟机占8个字节。
类型指针
即对象指向它的类型元数据的指针。Java虚拟机通过这个指针来确定该对象是哪个类的实例。
并不是所有虚拟机的实现都必须在对象数据上保留类型指针。
32位虚拟机或64为虚拟机(-XX:+UseCompressedOops,开启指针压缩,默认开启)占用4个字节。
64为虚拟机(无指针压缩)占8个字节。
数组长度
如果对象是数组,额外需要占用4个字节。
实例数据
实例数据是对象真正存储的有效信息,即代码中定义的各种类型的字段内容。
| 类型 | 大小(字节) |
|---|---|
| byte | 1 |
| boolean | 1 |
| char | 2 |
| short | 2 |
| int | 4 |
| float | 4 |
| long | 8 |
| double | 8 |
| ref | 32位或64位虚拟机(开启指针压缩)引用类型占用4个字节, 64位虚拟机引用类型占用8个字节。 |
字段的存储顺序
受虚拟机分配策略参数(-XX:FieldsAllocationStyle)和字段在源码中的定义顺序影响。
默认的顺序:
longs/doubles、ints、shorts/chars、bytes/booleans、oops
相同宽度的总被分配在一起,在满足这个前提的情况下,父类的变量会出现在子类之前。
如果XX:FieldsAllocationStyle=0(默认是1),那么oops会在最前边。
如果HotSpot虚拟机的+XX:CompactFields参数为true(默认true),那么子类之中较
窄的变量也允许插入到父类变量的空隙之中。
对齐填充
由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,
也就是任何对象的大小必须是8字节的整数倍。
查看对象内存布局
可利用jol查看对象内部布局,注意大小端。
public static void main(String[] args) throws InterruptedException {
/*
jvm 默认开启偏向锁(-XX:+UseBiasedLocking)
在jvm启动后4s开启(-XX:BiasedLockingStartupDelay=4000),会看到101
TimeUnit.SECONDS.sleep(5);
*/
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 加锁之后可以看到对象头的锁标识发生改变
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.11</version>
</dependency>