Java中的对象头(Object Header)与锁优化机制详解
一、知识描述
在Java中,每个对象在内存中都包含一个对象头(Object Header)。对象头记录了对象的元数据信息,是JVM进行内存管理、垃圾回收、锁优化和多线程同步的关键数据结构。理解对象头的内容和布局,是深入学习Java并发、内存模型和JVM优化的基础。对象头中尤其重要的部分是Mark Word,它在不同锁状态(如无锁、偏向锁、轻量级锁、重量级锁)下会存储不同的信息,以支持高效的锁优化机制。
二、对象头(Object Header)的组成
一个典型的Java对象在内存中(以64位JVM为例,未开启指针压缩)的结构如下:
| 组成部分 | 大小(字节) | 说明 |
|---|---|---|
| Mark Word | 8 | 存储对象的运行时数据,如哈希码、锁状态、GC分代年龄等 |
| Klass Pointer | 8 | 指向对象所属类的元数据(即Class对象)的指针 |
| 数组长度(可选) | 4 | 仅数组对象有,记录数组长度 |
| 实例数据 | 不定 | 对象的实例字段(包括父类继承的) |
| 对齐填充 | 不定 | 保证对象大小是8字节的整数倍(内存对齐) |
注意:在64位JVM开启指针压缩(默认开启,-XX:+UseCompressedOops)时,Klass Pointer会压缩为4字节;Mark Word在32位JVM中占4字节,64位JVM占8字节。
三、Mark Word的详细结构
Mark Word是对象头的核心,其内容会随着锁状态的变化而复用同一块内存空间(在不同状态下存储不同的信息)。以64位JVM为例,Mark Word的结构如下(图中每个小格代表1个比特位,总共64位):
|-----------------------------------------------------------------------|
| Mark Word (64 bits) |
|-----------------------------------------------------------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
|-----------------------------------------------------------------------|
- 这是无锁状态下的布局:
hashcode:31:对象的哈希码(第一次调用hashCode()方法时计算并存储)。age:4:对象的分代年龄(用于GC,Young GC存活一次年龄+1,达到阈值晋升老年代)。biased_lock:1:是否启用偏向锁(1表示启用,0表示禁用)。lock:2:锁标志位,这里是01(无锁状态)。
当锁状态变化时,Mark Word的布局会完全改变,以存储不同信息:
| 锁状态 | lock标志位 | biased_lock位 | 存储内容说明 |
|---|---|---|---|
| 无锁 | 01 | 0 | 哈希码、分代年龄等 |
| 偏向锁 | 01 | 1 | 持有偏向锁的线程ID、Epoch、分代年龄 |
| 轻量级锁 | 00 | 无 | 指向栈中锁记录的指针 |
| 重量级锁 | 10 | 无 | 指向互斥量(monitor)的指针 |
| GC标记 | 11 | 无 | 空(用于垃圾回收) |
四、锁优化机制详解
Java的synchronized锁从无锁到重量级锁的升级过程,就是基于Mark Word的变化实现的。这个过程是不可逆的升级(偏向锁 → 轻量级锁 → 重量级锁),目的是在保证线程安全的同时减少锁开销。
1. 无锁状态
- 对象刚创建时,默认处于无锁状态。
- 如果对象没有重写
hashCode(),则第一次调用该方法时才会计算哈希码并存入Mark Word。
2. 偏向锁
- 目的:在没有竞争的情况下,消除整个同步的开销(如
synchronized方法)。 - 原理:
- 当第一个线程访问同步块时,会在Mark Word中记录该线程的ID(54位,实际是当前线程指针的哈希),并将偏向锁标志位设为1。
- 之后该线程再进入同步块时,只需检查Mark Word中线程ID是否为自己,如果是则直接执行,无需任何原子操作。
- 批量重偏向/撤销:
- 如果多个线程竞争,但偏向锁不是同一个线程,JVM会批量重偏向(Bulk Rebias)或批量撤销(Bulk Revoke),避免频繁锁升级。
- 延迟启用:JVM启动后偏向锁默认延迟4秒启用(避免启动时竞争激烈)。
3. 轻量级锁
- 触发条件:当有另一个线程尝试获取偏向锁时(说明有竞争),偏向锁会升级为轻量级锁。
- 加锁过程:
- 在当前线程的栈帧中创建锁记录(Lock Record),将Mark Word复制到锁记录中(Displaced Mark Word)。
- 通过CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针。
- 如果成功,当前线程获得锁;如果失败(说明有竞争),会自旋尝试(忙等待)。
- 解锁过程:用CAS将Displaced Mark Word替换回对象头,如果成功则解锁;如果失败,说明已升级为重量级锁,进入重量级锁解锁流程。
4. 重量级锁
- 触发条件:轻量级锁自旋失败(默认自旋10次,可用
-XX:PreBlockSpin调整)或等待线程数太多(超过CPU核心数的一半)。 - 原理:
- 对象头中的Mark Word会指向一个监视器(Monitor),即操作系统层面的互斥量(mutex)。
- 线程会进入操作系统的等待队列,由操作系统进行调度,涉及用户态到内核态的切换,开销大。
- 适用场景:高并发、长时间持有的锁。
五、锁优化示例与调试
可以通过查看对象的内存布局来验证锁状态变化。使用OpenJDK的jol-core工具(Java Object Layout):
- 添加依赖(Maven):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
- 示例代码:
import org.openjdk.jol.info.ClassLayout;
public class ObjectHeaderExample {
public static void main(String[] args) throws Exception {
Object obj = new Object();
// 1. 无锁状态
System.out.println("=== 无锁状态 ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 2. 偏向锁(需等待偏向锁延迟结束)
Thread.sleep(5000); // 等待偏向锁启用
Object biasedObj = new Object();
synchronized (biasedObj) {
System.out.println("=== 偏向锁状态 ===");
System.out.println(ClassLayout.parseInstance(biasedObj).toPrintable());
}
// 3. 轻量级锁
new Thread(() -> {
synchronized (obj) {
System.out.println("=== 轻量级锁状态 ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();
}
}
输出示例:
=== 无锁状态 ===
java.lang.Object 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)
...
# lock:01, biased_lock:0 → 无锁
=== 偏向锁状态 ===
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 30 40 1b (00000101 00110000 01000000 00011011) (456262661)
...
# lock:01, biased_lock:1 → 偏向锁
六、重要参数与调优
JVM提供了一些参数来控制锁行为:
-XX:+UseBiasedLocking:启用偏向锁(JDK 15后默认禁用,因为现代应用竞争激烈,偏向锁收益低)。-XX:BiasedLockingStartupDelay=4000:偏向锁延迟启用时间(毫秒)。-XX:+PrintFlagsFinal:查看所有JVM参数默认值。
七、总结
- 对象头是JVM管理对象的核心数据结构,包含Mark Word和类指针。
- Mark Word在不同锁状态下复用存储空间,存储哈希码、锁标志、线程ID等信息。
- 锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,目的是在竞争程度不同时平衡性能。
- 偏向锁适合无竞争场景,轻量级锁适合低竞争短时间同步,重量级锁适合高竞争。
- 可通过
jol-core工具查看对象内存布局,结合JVM参数进行调优。
思考:为什么JDK 15后默认禁用了偏向锁?(因为维护偏向锁需要额外开销,而现代多线程应用竞争激烈,偏向锁反而会带来性能下降,且撤销成本高。)