一、传统对象头:两间"房间"各司其职
在传统的 64 位 HotSpot JVM(开启指针压缩)中,每个 Java 对象头占用 12 字节(96 位)。它分成两个独立的部分:
房间 1:Mark Word(8 字节 / 64 位)
存放对象"元数据":
- 哈希码(identity hash code)------ 当调用
System.identityHashCode()后才会有 - GC 分代年龄(age)
- 锁状态(偏向锁、轻量级锁、重量级锁)
- 线程 ID(偏向锁时记录偏向的线程)
房间 2:Klass Pointer(4 字节 / 32 位)
指向 Metaspace 中类的元数据。因为指针压缩,它从 8 字节压缩到了 4 字节。
设计的关键特征:两个房间物理上是分开的。Mark Word 是一个独立的 64 位,Klass Pointer 是另一个独立的 32 位(外加 4 字节对齐填充)。
二、紧凑对象头的核心设计思想:把两个房间合并成一个
Project Lilliput 的设计者意识到:Mark Word 中有很多位并不是永远都用的上。比如:
- 哈希码 31 位,但大多数对象可能永远不会调用
identityHashCode()。 - 锁状态位只需要几个 bit。
- GC 年龄只需要 4 bit。
而 Klass Pointer 被压缩到 32 位之后,实际上只要 32 位就够了。
所以设计者提出的新方案是:用一个 64 位的结构同时存储原 Mark Word 中的必要信息 + 压缩后的类指针。也就是把两个房间合并成一个 64 位的"集成房间"。
三、位布局设计:每一 bit 都有名字
紧凑对象头的 64 位分配如下(从低位到高位):
| 位范围 | 用途 | 位数 | 说明 |
|---|---|---|---|
| 0-1 | 锁状态 | 2 | 无锁、轻量级锁、重量级锁等 |
| 2-6 | GC 状态 | 5 | 与锁状态配合,供 GC 使用(如年龄、标记等) |
| 7-31 | 身份哈希码 | 25 | 只在真正需要时写入 |
| 32-63 | 压缩类指针 | 32 | 指向类元数据的压缩指针(同传统压缩指针) |
这样,所有原 Mark Word 中真正必要的信息被压缩到 32 位之内(前 32 位),剩下的 32 位专门存放类指针。
你可能会问:25 位哈希码够用吗?传统是 31 位。25 位意味着最多约 3300 万个唯一哈希值,冲突概率在实际应用中可接受。这也是设计时的权衡 ------ 用极少碰撞的代价换来 6 个 bit 的空间。
四、设计的关键技巧:延迟初始化 + 借用填充位
技巧 1:哈希码不是预先占位
传统对象头在对象创建时并不会写哈希码,只有当你调用 identityHashCode() 时才会写入。紧凑对象头也一样 ------ 哈希码字段(25 位)一开始为空,需要时再填充。这就节省了大量对象头的"预留浪费"。
技巧 2:利用对象尾部的填充位
JVM 要求对象按 8 字节对齐,所以对象结尾可能会有 0~7 字节的填充。如果哈希码 + 类指针等信息实在放不下(例如哈希码 25 位不够用?实际上几乎不会),设计师们巧妙地把额外的哈希码放在对象末尾的填充字节里。这样不会破坏对象头的固定布局,既保持了快速访问,又避免了通过 GC 迁移对象存储哈希码的开销。
技巧 3:原型头部(prototype header)加速分配
每个类在 Metaspace 中预先计算好一个"原型头部" ------ 一个已经填好类指针和默认锁状态的 64 位值。当创建对象时,JIT 编译器直接把这个 64 位值写入对象起始位置,不需要再去分别设置 Mark Word 和 Klass Pointer。这实际上比传统分配更快,因为传统分配需要写入两个不连续的内存区域。
五、设计上的取舍:牺牲了什么,保留了什么?
保留了:
- 所有锁功能(偏向锁、轻量锁、重量锁)
- GC 分代年龄
- 类指针寻址
- 绝大多数对象的身份哈希码(25 位哈希冲突极低)
牺牲/改变了:
- 哈希码从 31 位降为 25 位
- 不再有独立于类指针的 Mark Word 空间
- 未来某些极端情况(如 25 位哈希冲突后再次调用
identityHashCode())可能需要额外手段处理(例如借用对象填充位)
不可用的功能:
- 偏向锁在 JDK 21 之后已经被默认禁用,甚至可能被移除,所以紧凑对象头没有为偏向锁保留特定 bit。如果你仍然强制启用偏向锁(
-XX:+UseBiasedLocking),紧凑对象头不能工作,JVM 会自动退回传统布局。
六、形象的类比
可以把传统对象头想象成一间两卧室公寓:
- 卧室 A(Mark Word):8 平米,放一些日常元数据。
- 卧室 B(Klass Pointer):4 平米,放类的门牌号。
- 两个卧室之间有 4 平米走廊(对齐填充)。
紧凑对象头则改成了一间开放式大客厅:
- 总面积 8 平米,用"活动隔断"同时放日常元数据和类门牌号。
- 客厅里设计了一套折叠家具:当你不需要哈希码时,空间留给别的;当需要时,从墙体隐藏处拉出来(借用对象尾部填充)。
- 整体面积更小,但功能一个不少。
七、总结:设计精髓就这 4 句话
- 合并:打破 Mark Word 与 Klass Pointer 物理隔离的传统,把两者塞进同一个 64 位字。
- 压缩位宽:将锁、GC、哈希码等必要信息压缩到前 32 位,类指针占后 32 位。
- 延迟+借用:哈希码按需生成,必要时利用对象尾部填充位做扩展。
- 预计算原型头部:让对象分配路径更短,性能不降反升。
理解了这四点,你就真正掌握了 JEP 534 紧凑对象头的设计思想。它不是什么黑魔法,而是一系列精妙的信息打包与时空置换策略,用微小的哈希冲突概率和有限的偏向锁牺牲,换来堆内存平均减少 22%、GC 压力显著降低的巨大收益。
如果你希望我画一个位布局的直观示意图 或传统 vs 紧凑的对比表格,我可以再用 ASCII 或文字描述得更直观。
本回答由 AI 生成,内容仅供参考,请仔细甄别。