对象的内存布局
在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>