Mark Word 位分配与年龄位压缩的真相

116 阅读2分钟

Mark Word 有 64 位空间,hashCode 只占 31 位,为什么会导致 GC 年龄位从 4 位压缩到 2 位? 这涉及到 HotSpot JVM 对象头的精细节约机制:

graph TD
    A[64位 Mark Word] --> B[多模式复用]
    B --> C[无锁状态]
    B --> D[偏向锁状态]
    B --> E[重量锁状态]
    B --> F[GC标记状态]

    C --> C1[哈希码31位]
    C --> C2[分代年龄4位]
    C --> C3[锁标志2位]
    C --> C4[未使用27位]

    D --> D1[线程ID54位]
    D --> D2[时间戳2位]
    D --> D3[年龄4位]
    D --> D4[锁标志2位]

    E --> E1[指向Monitor指针62位]
    E --> E2[锁标志2位]

    F --> F1[GC信息62位]
    F --> F2[锁标志2位]

一、位分配冲突的核心原因

1. 哈希码与偏向锁互斥

graph LR
    无锁状态 --> 可存哈希码
    偏向锁状态 --> 需存线程ID

    冲突点 --> 同一空间[相同物理存储位置]
    同一空间 --> 不可共存[无法同时存储]
  • 关键限制:哈希码(31位)和偏向锁信息(54位线程ID+2位时间戳)共享相同的位域
  • 设计选择:HotSpot 优先保证锁状态信息的完整性

2. 状态转换规则

stateDiagram-v2
    [*] --> 无锁
    无锁 --> 偏向锁: 尝试获取偏向锁
    无锁 --> 哈希码: 调用hashCode()

    偏向锁 --> 哈希码: 禁止转换
    哈希码 --> 偏向锁: 禁止转换
  • 不可逆操作:一旦写入哈希码,对象永远无法进入偏向锁状态
  • 空间重组:禁用偏向锁后,原本用于偏向锁的空间被重新分配

二、哈希码写入后的位重组

1. 重组后的布局

graph TD
    新无锁状态 --> H[哈希码31位]
    新无锁状态 --> Age[分代年龄2位]
    新无锁状态 --> Lock[锁标志2位]
    新无锁状态 --> Unused[未使用29位]

    Age --> 仅4种可能[0-3]

2. 位压缩原因

graph TD
    需求[需求] --> 哈希码[必须保留31位哈希码]
    需求 --> 锁标志[必须保留2位锁标志]
    剩余空间[64-31-2=31位剩余] --> 决策[分配决策]
    决策 --> 分代年龄[分代年龄位]
    决策 --> 其他状态[GC/锁升级状态]
    分代年龄 --> 最小化[仅分配2位]
    原因 --> 优先级[年龄位优先级最低]
  • 空间权衡
    • 锁状态转换需要预留空间(轻量级锁/重量级锁)
    • GC 标记需要空间(三色标记等)
    • 分代年龄成为牺牲品

三、技术实现验证

1. HotSpot 源码证据

在 OpenJDK 源码 markOop.hpp 中:

enum {
  age_bits                 = 4, // 正常4位
  hash_bits                = 31 // 哈希码31位
};

// 调用hashCode后的布局
enum {
  locked_value             = 0,  // 00 轻量锁
  unlocked_value           = 1,  // 01 无锁
  monitor_value            = 2,  // 10 重量锁
  marked_value             = 3,  // 11 GC标记

  // 无锁状态位分配(当有哈希码时)
  hash_bits                = 31,
  age_bits                = 2,  // 从4位降为2位!
  lock_bits               = 2,
  unused_bits             = 64 - (hash_bits + age_bits + lock_bits)
};

2. 位分配数学证明

64位总空间:
- 哈希码固定占用 31位
- 锁标志固定占用 2位
- 剩余 31位

剩余31位分配:
- 分代年龄:2位(0-3)
- 锁状态/GC标记:需要约25位
- 未使用:4位

四、为什么不是其他方案?

1. 未采用的替代方案

graph TD
    方案A[减少哈希码位数] --> 问题[哈希冲突增加]
    方案B[动态位分配] --> 问题[状态转换复杂]
    方案C[外挂存储] --> 问题[性能下降]

    选择 --> 当前方案[压缩年龄位]
    原因 --> 最小代价[年龄影响相对较小]

2. 设计哲学

pie
    title 位分配优先级
    "锁状态信息" : 45
    "对象身份(哈希)" : 35
    "GC标记" : 15
    "分代年龄" : 5

五、影响与最佳实践

1. 实际影响范围

graph TD
    影响 --> 晋升[对象提前晋升]
    影响 --> 锁[偏向锁永久禁用]
    影响 --> 暂停[GC暂停微增]

    量化 --> 年龄限制[最大年龄=3]
    量化 --> 晋升加速[提前5倍]

2. 解决方案

flowchart LR
    避免 --> 不调用[避免调用hashCode]
    替代 --> 字段存储[独立字段存哈希]
    延迟 --> 老年代计算[晋升后计算]

    字段存储 --> 实现[添加hash字段]
    实现 --> 示例[SafeKey类]

六、与其他JVM对比

1. ZGC/Shenandoah

graph LR
    无分代 --> 无年龄位
    染色指针 --> 外挂元数据

    优势 --> 无此问题

2. IBM J9

graph TD
    不同布局[对象头分离] --> 独立哈希区
    独立哈希区 --> 无冲突

​结论​​:

虽然 Mark Word 有 64 位空间,但因多模式复用状态互斥机制,当存储 31 位哈希码时,HotSpot JVM 选择压缩分代年龄位(4位→2位)来保证核心功能。

这是设计权衡的结果:牺牲年龄精度换取对象身份和锁状态的完整存储。

最佳解决方案是在自定义对象中使用独立字段存储哈希码,绕过对象头限制。