mark word 动态复用同一内存区域,依对象状态切换存储哈希值、锁信息、gc年龄等;轻量级锁会覆盖哈希值,导致hashcode()结果变化,引发并发容器异常。

Mark Word 里到底存了啥
Java 对象头中的 Mark Word 是个 64 位(64 位 JVM)或 32 位(32 位 JVM)的紧凑字段,它不是固定存某一种东西,而是**动态复用同一块内存**,根据对象所处状态切换用途。你查到的“哈希值”“锁标志位”“GC 分代年龄”等,全挤在这一个字长里,靠低位的几个比特(lock bits)来区分当前模式。
哈希值和锁标志位为什么会冲突
调用过 System.identityHashCode() 或 Object.hashCode()(未被重写)的对象,JVM 会在首次计算时把哈希值塞进 Mark Word;但一旦对象被加锁(比如进入 synchronized 块),JVM 就得腾地方存锁记录指针、线程 ID、epoch 等——哈希值就得被挤走,甚至丢弃(轻量级锁升级后可能不再恢复)。这不是 bug,是设计取舍:空间比“永远保留哈希”更重要。
- 没锁且没算过哈希:低位 2–3 bit 是
01(无锁态),高位存分代年龄或偏向线程 ID - 算过哈希但没锁:低位是
01,高位存哈希值(31 位或 61 位,取决于 JVM 位数) - 轻量级锁:低位变成
00,原哈希值被覆盖,指向栈中Lock Record - 偏向锁:低位是
01,但含义变了,高位存偏向线程 ID + epoch,哈希值已不可恢复
为什么 hashCode() 调用后又加锁,结果可能变
这是最常踩的坑:你以为对象哈希值是稳定的,但只要它后续被加锁(哪怕只是 synchronized 方法里临时一进),再读 hashCode() 可能返回新值(JVM 在哈希丢失后会重新生成一个,通常是基于地址再散列)。尤其在并发容器里用自定义 key 时,如果 key 的 hashCode() 依赖对象身份,又在多线程里被同步访问,就可能触发 HashMap 键错位、ConcurrentHashMap 扩容异常等隐性问题。
- 避免在会被加锁的对象上依赖
System.identityHashCode()做业务逻辑(比如分片、路由) - 若必须稳定哈希,自己缓存到 final 字段:
private final int cachedHash = System.identityHashCode(this); - JDK 17+ 的
-XX:+UseCompactObjectHeaders会进一步压缩Mark Word,加剧哈希与锁的互斥,别盲目开启
怎么验证当前对象的 Mark Word 状态
没有标准 API 直接读 Mark Word,但可以用 Unsafe 配合对象地址粗略窥探(仅限调试,生产禁用):
立即学习“Java免费学习笔记(深入)”;
Unsafe unsafe = Unsafe.getUnsafe();
long offset = unsafe.objectFieldOffset(Object.class.getDeclaredField("hashCode"));
// 实际需通过内存地址 + header size 计算,更可靠的方式是用 JOL(Java Object Layout)工具
更实用的办法是用 jol-cli 工具:java -jar jol-cli.jar internals java.lang.Object,它会显示不同锁状态下 Mark Word 的二进制布局。注意:输出受 JVM 参数影响极大,-XX:+UseBiasedLocking 开关会直接改变低位编码规则。
真正难的不是看懂结构,而是意识到:同一个字段,在不同时间点、不同线程操作下,语义完全不同。你写的代码如果隐式依赖它的某个快照状态(比如以为哈希一直存在),运行时就可能悄无声息地出错。










