前言
本文主要分享不同锁状态下,Java对象头的布局是怎么样的,重点关注其中的Mark Word字段。文章构成如下:Mark Wrod 字段对照表、锁状态表、JOL查看对象头实战、关于偏向锁的特殊说明。
Mark Word字段对照表
| English 英文 | Chinese 中文 | Description 描述 |
|---|---|---|
| bits | 位 | Basic unit of digital information 数字信息的基本单位 |
| unused | 未使用 | Reserved space, not currently used 保留空间,当前未使用 |
| identity_hashcode | 对象哈希码 | Unique hash code of the object 对象的唯一哈希码 |
| age | GC年龄 | Generation count in garbage collection 垃圾回收中的分代计数 |
| thread | 线程ID | Identifier of the thread holding the lock 持有锁的线程标识符 |
| epoch | 时间戳/版本号 | Timestamp or version for biased locks 偏向锁的时间戳或版本号 |
| ptr_to_lock_record | 指向栈中锁记录的指针 | Pointer to lock record in thread stack 指向线程栈中锁记录的指针 |
| ptr_to_monitor | 指向Monitor对象的指针 | Pointer to ObjectMonitor object 指向ObjectMonitor对象的指针 |
| Normal (Unlocked) | 无锁状态 | No thread holds the lock 无线程持有锁 |
| Biased Lock | 偏向锁 | Lock biased towards first acquiring thread 偏向于第一个获取锁的线程 |
| Thin Lock | 轻量级锁 | Lock using CAS and stack records 使用CAS和栈记录的锁 |
| Fat Lock | 重量级锁 | Lock using OS-level monitor 使用操作系统级监视器的锁 |
| GC Marked | GC标记 | Object marked during garbage collection 垃圾回收期间标记的对象 |
状态表
| 锁状态 | 锁标志位 (2 bits) | 偏向锁标志 (1 bit) | Mark Word 64位结构 (高位 → 低位) | 存储内容说明 |
|---|---|---|---|---|
| 无锁状态 | 01 | 0 | 25 bits unused | 31 bits identity_hashcode | 1 bit unused | 4 bits age | 0 | 01 | • 31位:对象哈希码(第一次调用hashCode()时生成) • 4位:GC分代年龄(0-15) • 25位:未使用 • 1位:未使用 |
| 偏向锁状态 | 01 | 1 | 54 bits thread | 2 bits epoch | 1 bit unused | 4 bits age | 1 | 01 | • 54位:持有偏向锁的线程ID • 2位:epoch(偏向锁时间戳) • 4位:GC分代年龄 • 1位:未使用 |
| 轻量级锁状态 | 00 | - | 62 bits ptr_to_lock_record | 00 | • 62位:指向栈中锁记录的指针 • 指针最后2位实际为00(对齐要求) |
| 重量级锁状态 | 10 | - | 62 bits ptr_to_monitor | 10 | • 62位:指向ObjectMonitor对象的指针 • 指针指向堆中的Monitor实例 |
| GC标记状态 | 11 | - | 62 bits GC information | 11 | • 62位:垃圾收集相关信息 • 不同GC算法内容不同 |
JOL工具查看对象头
JOL(Java Object Layout)是OpenJDK提供的官方工具,专门用于分析Java对象的内存布局。在项目中引入相关代码依赖可以参考如下:
Maven:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
<scope>provided</scope>
</dependency>
Gradle:
dependencies {
compileOnly 'org.openjdk.jol:jol-core:0.17'
}
下面给出一个不同锁状态下查看对象头布局的代码示例,感兴趣的可以本地运行:
package concurrent;
import org.openjdk.jol.info.ClassLayout;
/**
* 使用 JOL(Java Object Layout)查看 Java 对象头在不同锁状态下的变化。
* 顺序:无锁 → 无锁(含 hash) → 偏向锁 → 轻量级锁 → 重量级锁。
*
* 要看到「偏向锁」必须开启 JVM 偏向锁并去掉启动延迟,推荐运行:
* ./gradlew runObjectHeaderDemo
* (该任务已自动添加 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0)
*
* 若仍用 runMain 且未加上述 VM 参数,JDK 15+ 下第 3 步会显示 thin lock 而非 biased lock。
* JDK 18+ 已移除偏向锁,只能看到轻量级/重量级。
*/
public class ObjectHeaderDemo {
static class SimpleObject {
int x = 42;
long y = 100L;
}
public static void main(String[] args) throws InterruptedException {
// 用于展示:无锁 → 无锁+hash → 轻量级 → 重量级(此对象会先算 hash,因此不会进入偏向)
SimpleObject obj = new SimpleObject();
// 1. 无锁
System.out.println("=== 1. 初始状态(无锁) ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 2. 无锁但已写入 identity hashCode(一旦写入 hash,该对象永远无法进入偏向锁)
int hc = System.identityHashCode(obj);
System.out.println("identityHashCode = 0x" + Integer.toHexString(hc));
System.out.println("\n=== 2. 写入 hashCode 后(无锁,且此对象之后无法偏向) ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 3. 偏向锁:必须用「从未算过 hash」的对象,且要等偏向生效(默认约 4s)
SimpleObject biasedCandidate = new SimpleObject();
System.out.println("\n=== 3. 偏向锁(等待 " + 5 + " 秒让 JVM 开启该类偏向,且此对象从未调用 identityHashCode) ===");
Thread.sleep(5000);
synchronized (biasedCandidate) {
System.out.println(ClassLayout.parseInstance(biasedCandidate).toPrintable());
}
// 4. 轻量级锁:对已写 hash 的 obj 加锁,只能是轻量级
System.out.println("\n=== 4. 轻量级锁(同一线程对 obj 加锁) ===");
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 5. 多线程竞争 → 重量级锁
System.out.println("\n=== 5. 多线程竞争下的对象头(可能膨胀为重量级锁) ===");
showContendedLockStates(obj);
}
/**
* 展示在多线程竞争同一个锁时,对象头的变化。
*/
private static void showContendedLockStates(SimpleObject obj) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (obj) {
System.out.println("T1 获取到锁时的对象头:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 保持一段时间,让 T2 有机会竞争这把锁
sleepQuietly(500);
}
System.out.println("T1 释放锁后结束");
}, "T1-holder");
Thread t2 = new Thread(() -> {
// 先稍微等待,尽量保证在 T1 已经持锁的情况下来竞争
sleepQuietly(50);
synchronized (obj) {
System.out.println("T2 竞争并获取到锁时的对象头:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("T2 释放锁后结束");
}, "T2-contender");
System.out.println("主线程:启动 T1、T2 之前的对象头:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程:T1、T2 都结束后的对象头:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
private static void sleepQuietly(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
以上代码在我本地运行的结果,可参考如下:
code git:(main) ✗ ./gradlew runObjectHeaderDemo
> Task :runObjectHeaderDemo
OpenJDK 64-Bit Server VM warning: Option UseBiasedLocking was deprecated in version 15.0 and will likely be removed in a future release.
OpenJDK 64-Bit Server VM warning: Option BiasedLockingStartupDelay was deprecated in version 15.0 and will likely be removed in a future release.
=== 1. 初始状态(无锁) ===
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
identityHashCode = 0x15ff3e9e
=== 2. 写入 hashCode 后(无锁,且此对象之后无法偏向) ===
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000015ff3e9e01 (hash: 0x15ff3e9e; age: 0)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
=== 3. 偏向锁(等待 5 秒让 JVM 开启该类偏向,且此对象从未调用 identityHashCode) ===
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fb3c9008805 (biased: 0x0000001fecf24022; epoch: 0; age: 0)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
=== 4. 轻量级锁(同一线程对 obj 加锁) ===
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000c139ab0 (thin lock: 0x000070000c139ab0)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
=== 5. 多线程竞争下的对象头(可能膨胀为重量级锁) ===
主线程:启动 T1、T2 之前的对象头:
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000015ff3e9e01 (hash: 0x15ff3e9e; age: 0)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
T1 获取到锁时的对象头:
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000d59fa10 (thin lock: 0x000070000d59fa10)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
T1 释放锁后结束
T2 竞争并获取到锁时的对象头:
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fb3d0013712 (fat lock: 0x00007fb3d0013712)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
T2 释放锁后结束
主线程:T1、T2 都结束后的对象头:
concurrent.ObjectHeaderDemo$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fb3d0013712 (fat lock: 0x00007fb3d0013712)
8 4 (object header: class) 0x00000a30
12 4 int SimpleObject.x 42
16 8 long SimpleObject.y 100
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
关于偏向锁的特殊说明
Java高版本逐渐移除偏向锁,移除时间线:
- JDK 15: 默认禁用偏向锁(
-XX:-UseBiasedLocking) - JDK 18: 完全移除偏向锁相关代码
- 当前状态: 所有现代JVM版本(JDK 18+)都不支持偏向锁
据我所观察,偏向锁未出现通常有三个可能原因:
- 先调用了 System.identityHashCode(obj),对象头里写入了 hash,就无法再进入偏向锁。
- 偏向有“延迟生效”(默认约 4 秒),刚 new 出来的对象不会立刻偏向。
- JDK版本较高,JVM默认未开启或者已经移除偏向锁的使用。
在我本地项目运行过程中使用的是JDK17,可以在运行时加入以下JVM参数:
// 运行对象头示例并开启偏向锁(仅 JDK 8~17 有效,JDK 18+ 已移除偏向锁)
tasks.register<JavaExec>("runObjectHeaderDemo") {
group = "application"
description = "Run ObjectHeaderDemo with -XX:+UseBiasedLocking to see biased lock state"
javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17))
})
mainClass.set("concurrent.ObjectHeaderDemo")
classpath = sourceSets["main"].runtimeClasspath
jvmArgs(
"-XX:+UseBiasedLocking",
"-XX:BiasedLockingStartupDelay=0",
"-Djdk.attach.allowAttachSelf=true" // 让 JOL 能 attach 当前进程,避免 ClassLoader 重复加载导致异常
)
}