介绍

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
实例数据:存放类的属性数据信息,包括父类的属性信息;
对齐填充:仅仅是为了字节对齐,虚拟机要求对象起始地址必须是8字节的整数倍。填充的数据不是必须存在的;
对象头:Java对象头一般占有2个机器码,但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
▪ 虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。
ClassPointer是对象指向类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
MarkWord则是用于存储对象自身的运行时数据,比如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
Mark Word
引入 jol-core 依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public static void main(String[] args) throws InterruptedException {
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.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
通过打印出来的信息,可以看到我们使用的是64位 jvm,并开启了指针压缩,对象默认使用8字节对齐方式。
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
对象内存输出结果:

上图所示中的1,2,3分别对应了:
Mark Word 8字节
类型指针
对齐填充: 因为前面两部分 相加为8+4 = 12, 要是最后时8的倍数,所以补上4个字节
其余展示含义:
OFFSET:偏移地址,单位为字节
SIZE:占用内存大小,单位为字节
TYPE:Class中定义的类型
DESCRIPTION:类型描述,Obejct header 表示对象头,alignment表示对齐填充
VALUE:对应内存中存储的值
锁信息
参考《深入理解 Java 虚拟机》,各种锁在 MarkWord 中锁标识位的使用情况如下图所示:
对象Mark Word字节说明:

对照 对象内存输出结果可知,结果里表示锁信息的二进制是001, 所以是一个无锁状态。
HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁。试下如下情况:
public static void main(String[] args) throws InterruptedException {
//线程sleep 5s,确保虚拟机中偏向锁开启
Thread.sleep(5000);
new Thread(() -> {
Object o = new Object();
synchronized (o) {
//当前锁对象第一次被线程获取
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
}
偏向锁图:

在线程sleep 5s 并创建新县城去获取锁对象,因为当前只有一个线程来获取这个锁对象,虚拟机就会把标志位设置为偏向锁(101),并且将获取这个锁的线程ID放到对象的Mark Word 中,具体的字节对应可以将偏向锁图和Mark Word字节说明对应来看
模拟重量级锁:
static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
//线程sleep 5s,确保虚拟机中偏向锁开启
Thread.sleep(5000);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
synchronized (o) {
//当前锁对象第一次被线程获取
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
}
}
重量级锁图:

实例数据
实例数据(Instance Data)保存的是对象真正存储的有效信息,保存了代码中定义的各种数据类型的字段内容,并且如果有继承关系存在,子类还会包含从父类继承过来的字段。
Type | Bytes |
byte,boolean | 1 |
char,short | 2 |
int,float | 4 |
long,double | 8 |
public class Customer {
private Long id;
private Integer status;
private Long createdAt;
private Long updatedAt;
private String uniqueCode;
}

注意:
属性的排列顺序与在类中定义的顺序不同,这是因为jvm会采用字段重排序技术,对原始类型进行重新排序,以达到内存对齐的目的。具体规则遵循如下:
按照数据类型的长度大小,从大到小排列
具有相同长度的字段,会被分配在相邻位置
如果一个字段的长度是L个字节,那么这个字段的偏移量(OFFSET)需要对齐至nL(n为整数)
对齐填充
在Hotspot的自动内存管理系统中,要求对象的起始地址必须是8字节的整数倍,也就是说对象的大小必须满足8字节的整数倍。因此如果实例数据没有对齐,那么需要进行对齐补全空缺,补全的bit位仅起占位符作用,不具有特殊含义。
在开启指针压缩的情况下,如果类中有long/double类型的变量时,会在对象头和实例数据间形成间隙(gap),为了节省空间,会默认把较短长度的变量放在前边,这一功能可以通过jvm参数进行开启或关闭:
# 开启
-XX:+CompactFields
# 关闭
-XX:-CompactFields
总结
锁升级过程的主体参与在Mark Word 中,可以对其进行深入了解。理解从无锁转换为偏向锁再至轻量级锁,重量级锁的变化。