Java 线程同步-02:不同锁状态下Java对象头的表现

15 阅读7分钟

前言

本文主要分享不同锁状态下,Java对象头的布局是怎么样的,重点关注其中的Mark Word字段。文章构成如下:Mark Wrod 字段对照表、锁状态表、JOL查看对象头实战、关于偏向锁的特殊说明。

Mark Word字段对照表

English 英文Chinese 中文Description 描述
bitsBasic unit of digital information 数字信息的基本单位
unused未使用Reserved space, not currently used 保留空间,当前未使用
identity_hashcode对象哈希码Unique hash code of the object 对象的唯一哈希码
ageGC年龄Generation count in garbage collection 垃圾回收中的分代计数
thread线程IDIdentifier 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 MarkedGC标记Object marked during garbage collection 垃圾回收期间标记的对象

状态表

锁状态锁标志位 (2 bits)偏向锁标志 (1 bit)Mark Word 64位结构 (高位 → 低位)存储内容说明
无锁状态01025 bits unused | 31 bits identity_hashcode | 1 bit unused | 4 bits age | 0 | 01• 31位:对象哈希码(第一次调用hashCode()时生成)
• 4位:GC分代年龄(0-15)
• 25位:未使用
• 1位:未使用
偏向锁状态01154 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+)都不支持偏向锁

据我所观察,偏向锁未出现通常有三个可能原因:

  1. 先调用了 System.identityHashCode(obj),对象头里写入了 hash,就无法再进入偏向锁。
  2. 偏向有“延迟生效”(默认约 4 秒),刚 new 出来的对象不会立刻偏向。
  3. 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 重复加载导致异常
    )
}