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

前言

本文主要分享不同锁状态下,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:

xml 复制代码
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
    <scope>provided</scope>
</dependency>

Gradle:

gradle 复制代码
dependencies {
    compileOnly 'org.openjdk.jol:jol-core:0.17'
}

下面给出一个不同锁状态下查看对象头布局的代码示例,感兴趣的可以本地运行:

java 复制代码
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();
        }
    }
}

以上代码在我本地运行的结果,可参考如下:

shell 复制代码
 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参数:

java 复制代码
// 运行对象头示例并开启偏向锁(仅 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 重复加载导致异常
    )
}
相关推荐
青云计划12 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor35612 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor35612 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11113 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai14 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
+VX:Fegn089514 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟14 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事15 小时前
SQLAlchemy 技术入门指南
后端·python
识君啊15 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端
想用offer打牌16 小时前
MCP (Model Context Protocol) 技术理解 - 第五篇
人工智能·后端·mcp