对象头包含Mark Word和Klass Pointer两部分:Mark Word动态存储哈希码、GC年龄、锁状态等;Klass Pointer指向类的Klass元数据,64位开启压缩时为4字节,加8字节Mark Word及4字节对齐填充共16字节。

对象头里到底存了哪些字段
OpenJDK(HotSpot)对象头不是固定结构,而是分两块动态复用:Mark Word 和 Klass Pointer。这两部分加起来,在 64 位 JVM 默认开启压缩指针时共占 12 字节(Mark Word 8 字节 + Klass Pointer 4 字节),再加 4 字节对齐填充,凑成 16 字节最小对象大小。
常见误区是以为对象头只存“锁信息”或“哈希码”——其实它得在不同场景下反复切换用途:
-
Mark Word在无锁时存hash(identity hash)、age(GC 分代年龄)、biased_lock(是否启用偏向锁)、lock(锁状态:01 无锁、10 轻量级、11 重量级、01 偏向) - 加锁后,
Mark Word会被覆盖为指向栈中Lock Record的指针,或指向ObjectMonitor的指针 - 启用偏向锁且未撤销时,
Mark Word会存JavaThread*(持有线程 ID)和epoch(偏向时间戳) -
Klass Pointer始终指向该对象所属类的Klass元数据结构地址,是反射、类型检查、方法分派的起点
为什么不同对象的 JOL 输出里 offset 0–11 看起来一样但含义不同
因为 Mark Word 是位域复用结构,同一内存位置(比如 offset 0–7)在不同对象生命周期阶段承载不同语义。你用 ClassLayout.parseInstance(new Object()) 看到的 01000000 开头,并不表示“这个对象有哈希码”,而大概率是未初始化的默认值——hashCode() 没被调用前,hash 字段其实是空的(0),直到第一次调用才写入并置位。
验证方式很简单:
- 先打印未调用
hashCode()的对象布局:ClassLayout.parseInstance(new Object()).toPrintable() - 再调用一次
obj.hashCode(),重新打印,对比 offset 0–3 字节变化 - 加上
synchronized(obj)后再打,会发现整个Mark Word内容全变,甚至出现非 0x01 结尾的字节序列
压缩指针怎么影响对象头大小
Klass Pointer 是否压缩,直接决定对象头总长。64 位 JVM 默认开启 -XX:+UseCompressedClassPointers,此时 Klass Pointer 占 4 字节;若显式关闭(如加 -XX:-UseCompressedClassPointers),它就涨回 8 字节,对象头变成 16 字节(8+8),加上对齐填充后最小对象升至 24 字节。
注意:这和 -XX:+UseCompressedOops 控制的普通对象引用压缩是两回事,后者不影响对象头,只影响实例数据里的引用字段大小。
实操建议:
- 线上服务别关
UseCompressedClassPointers,否则每个对象多 4 字节,百万对象就是额外 4MB 内存 - 用
VM.current().details()可确认当前 JVM 是否启用了压缩类指针 - JOL 输出里如果看到
Klass Pointer占 4 字节,且Instance size是 16,基本可断定压缩已生效
对象头里没有存储的内容,常被误认为在里面
很多人查 dump 或看文档时会困惑:“为什么没看到 final 字段值?为什么没看到 volatile 标记?”——因为对象头不存业务字段,也不存 Java 语言层修饰符语义。
真正不在对象头里的关键信息包括:
- 对象的
final字段值 → 存在实例数据区,和普通字段一样布局 -
volatile字段的内存屏障语义 → 由 JIT 编译器插入指令保障,不占对象头空间 - 数组长度(
length)→ 数组对象头额外多 4 字节存length,但普通对象没有 - 类名、方法表、字段名等反射元数据 → 全在
Klass结构里,通过Klass Pointer间接访问,对象头只存指针本身
最容易被忽略的是:对象头里从不存任何字段的实际值,哪怕是一个 private static final int,它也放在类的静态变量区,跟单个对象头毫无关系。


















