Java 对象在 JVM 内存中有着固定的布局,通常由三部分组成:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中,对象头包含了至关重要的运行时元数据,特别是 Mark Word(标记字段),它记录了对象的哈希码、GC 年龄、锁状态等信息。
Java Object Layout (JOL) 是一个强大的工具,可以用来分析和可视化 Java 对象的内存布局,这对于理解 JVM 内部机制,特别是锁优化(如偏向锁)的原理至关重要。
1. JOL 引入
要使用 JOL,你需要在项目中引入 jol-core 依赖(以 Maven 为例):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
2. 观察对象头布局
我们首先观察一个简单对象的结构。在 64 位 JVM 且开启指针压缩(默认)的情况下,对象头通常占用 12 字节(Mark Word 8 字节,Class Pointer 4 字节)。
import org.openjdk.jol.info.ClassLayout;
public class JolObjectDemo {
public static void main(String[] args) {
Object o = new Object();
// 打印对象o的内存布局
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
输出(简化版):
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)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next field being aligne...)
Instance size: 16 bytes
前 8 字节是 Mark Word。注意 Mark Word 的最后三位:…001 表示对象处于无锁状态(且可偏向)。
3. 偏向锁的获取与观察
偏向锁(Biased Locking)是 JVM 对轻量级锁的进一步优化,用于提高在单个线程重复获取同一锁时的性能。默认情况下,JVM 启动后会有一段延迟(通常 4 秒)才开始启用偏向锁。为了实验方便,我们需要设置 JVM 参数来关闭这个延迟:
JVM Arguments: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
现在,我们观察一个对象如何被偏向:
public class BiasedLockingRevocationDemo {
public static void main(String[] args) throws InterruptedException {
// 确保JVM参数设置了 -XX:BiasedLockingStartupDelay=0
Object o = new Object();
Thread t1 = Thread.currentThread();
System.out.println("--- 1. 初始状态 (偏向可用) ---");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) { // T1 获取锁
System.out.println("--- 2. T1 获取偏向锁 ---");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
// 此时 Mark Word 最后三位应为 101,并包含 T1 的线程 ID
}
System.out.println("--- 3. T1 释放锁 (仍保持偏向) ---");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
Mark Word 状态变化解析:
- 初始状态: Mark Word 末尾为 …001 或 …101 (可偏向,但还未偏向任何线程)。
- T1 获取偏向锁: Mark Word 末尾为 …101。最关键的是,Mark Word 的中间部分不再是 0,而是记录了 T1 的线程 ID(Epoch 和 Thread ID)。
- T1 释放锁: Mark Word 状态不变,对象依然偏向于 T1。下次 T1 再进来时,无需 CAS 操作,直接判断 Mark Word 中的线程 ID 即可。
4. 偏向锁的撤销(Revocation)
偏向锁的撤销发生在两种主要情况下:
- 竞争撤销: 另一个线程(T2)尝试获取已被 T1 偏向的锁。
- 非锁操作撤销: 比如调用对象的 hashCode()。
由于哈希码需要占用 Mark Word 的空间,如果对象处于偏向锁状态,Mark Word 已经被线程 ID 占用,因此 JVM 会强制撤销偏向锁,将其升级为无锁或轻量级锁状态,并将哈希码写入。
我们使用调用 hashCode() 的方式观察撤销:
public class RevocationByHashCodeDemo {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
// T1 获取偏向锁
synchronized (o) {
System.out.println("--- 1. 初始获取偏向锁 ---");
// Mark Word: ...101 (偏向 T1)
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
// 强制调用 hashCode(),哈希码会写入 Mark Word,导致偏向锁撤销
System.out.println("对象 HashCode: " + o.hashCode());
System.out.println("--- 2. 调用 hashCode 后的状态 ---");
// Mark Word 末尾变为 ...000 (轻量级/重量级锁,但此时应是无锁状态且哈希码写入)
// 或 ...001 (无锁,哈希码写入)
System.out.println(ClassLayout.parseInstance(o).toPrintable());
// 再次尝试加锁,此时已无法进入偏向锁状态
synchronized (o) {
System.out.println("--- 3. 再次加锁 (不再是偏向锁) ---");
// 此时应升级为轻量级锁 (Mark Word 末尾 00)
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
通过 JOL 的输出,我们可以清晰地观察到 Mark Word 从记录线程 ID 的 …101 状态,经过 hashCode() 调用后,变成记录哈希码的无锁状态(通常末尾变为 …001),并且该对象将永久不能再进入偏向锁状态,从而印证了偏向锁撤销的细节和不可逆性。
汤不热吧