无锁状态下哈希码与分代年龄互斥复用Mark Word同一字段:未调用hashCode()时存4位age,调用后存31位hash,age移至GC相关数据结构中维护。

Java 中对象头在无锁状态下,哈希码和分代年龄(即 GC 年龄)是**共享存储在同一个 4 字节(32 位)字段中**的,具体布局取决于 JVM 实现(以 HotSpot 为主),且受是否启用 UseCompressedOops 和是否开启 hashCode() 计算影响。
对象头结构(无锁、未开启压缩指针时)
HotSpot 中普通对象的对象头由两部分组成:
- Mark Word(标记字):8 字节(64 位),存放哈希码、锁状态、GC 分代年龄、偏向线程 ID 等信息;
- Klass Pointer(类元数据指针):4 字节(压缩)或 8 字节(非压缩)。
重点在 Mark Word —— 它是动态复用的,不同状态含义不同。无锁状态下(即未被加锁、未被偏向),其低 3 位固定为 001(表示“无锁”状态),剩余 61 位用于存放其他信息。
哈希码与分代年龄如何共存?
它们**不同时存在**,而是互斥复用同一段空间:
- 若对象从未调用过
hashCode(),Mark Word 的 31 位(或 25 位,取决于是否压缩)用于存储 GC 分代年龄(age),默认最大值为 15(4 位),但实际 HotSpot 用 4 位存 age(0–15),其余位保留或作他用; - 一旦调用了
hashCode(),JVM 会将计算出的哈希码(32 位 int)写入 Mark Word 的对应区域,并将该对象标记为“已哈希”,此后 分代年龄不再存于 Mark Word 中,而是移至对象的其他位置(如与 GC 具体实现相关,例如 G1 中可能记录在 Remembered Set 或单独的 age table 中); - 也就是说:哈希码和分代年龄不会在同一时刻都“存”在 Mark Word 里 —— 哈希码优先抢占,且具有持久性(一旦生成就不会被覆盖)。
实际存储布局(以 64 位 HotSpot、无压缩指针为例)
无锁状态 Mark Word(64 位)典型格式如下(低位在右):
[ unused:25 | hash:31 | epoch:2 | age:4 | biased_lock:1 | lock:2 ] → 共 64 位
↑
低 3 位 = 001 表示无锁
说明:
- hash 占 31 位(足够存一个 32 位 int 的哈希码,最高位用 0 补齐);
- age 固定占 4 位(0–15),用于记录对象在 Survivor 区经历的 GC 次数;
- 但注意:这只是一个逻辑视图 —— 实际上当 hash 存在时,age 字段就不再更新,GC 过程中 age 信息由 JVM 在其它数据结构中维护;
- 如果对象未计算过 hashCode,则 hash 区域为 0,age 正常使用;一旦写入 hash,age 就“让位”,后续 GC 不再从对象头读取 age。
验证与注意事项
- 可通过
java.lang.instrument.Instrumentation.getObjectSize()或 JOL(Java Object Layout)工具查看对象头内容; - 调用
System.identityHashCode()或重写hashCode()后首次调用,会触发哈希码写入对象头(前提是未被锁); - 使用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly可观察底层锁膨胀和 mark word 修改过程(需 hsdis); - 开启
-XX:+UseBiasedLocking时,无锁状态可能变为“偏向锁状态”,此时对象头存储的是偏向线程 ID,哈希码和 age 都不可用,直到发生竞争撤销偏向后才恢复无锁布局。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











