Java 对象在内存中的布局是怎样的?
简化版
一个对象在堆里由三部分组成:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。对象头存「运行时元数据」——Mark Word(哈希码、GC 分代年龄、锁标志)和类型指针(指向类元信息);实例数据是各字段的值;对齐填充是补位,保证对象大小是 8 字节的整数倍。
详细版
① 对象头(Header),分两部分:
- Mark Word(64 位系统占 8 字节):存运行时数据,且内容随锁状态复用——无锁时存哈希码 + GC 分代年龄 + 锁标志位;加锁后存指向锁记录/Monitor 的指针或线程 ID。这是
synchronized锁升级的关键载体。 - 类型指针(Klass Pointer):指向方法区里该对象的类元信息,JVM 靠它知道「这是哪个类的对象」。开启指针压缩(默认)时占 4 字节。
- (数组还多一个 数组长度 字段。)
② 实例数据(Instance Data):对象真正的「内容」——所有字段的值(包括从父类继承的)。JVM 会按一定规则重排字段顺序以节省空间。
③ 对齐填充(Padding):不是必需的,只是用来凑整。HotSpot 要求对象起始地址是 8 字节的整数倍,不够就补 0。
所以一个 new Object() 在 64 位开启压缩指针时是 16 字节(8 Mark Word + 4 类型指针 + 4 填充)。
完整版教学
一、为什么要有对象头——对象的「身份证 + 状态栏」
实例数据是对象的「内容」,但 JVM 管理对象还需要一堆元数据:这对象是哪个类的?它的哈希码是多少?被锁了没?经历过几次 GC?这些信息不属于业务字段,就存在对象头里。
- 类型指针回答「你是谁」(哪个类);
- Mark Word 回答「你的运行时状态」(锁、哈希、GC 年龄)。
可以把对象头理解成每个对象自带的一小段「身份证 + 状态栏」,JVM 靠它来做类型识别、加锁、GC、哈希等底层操作。
二、Mark Word:按状态复用的元数据
Mark Word 会按对象状态复用有限位宽,常见 HotSpot 实现用它保存哈希、GC 年龄和锁相关状态。精确位图不是 JVM 规范,且同步实现已随 JDK 演进:偏向锁是旧版 HotSpot 优化:JDK 15 默认禁用并弃用相关选项,JDK 18 将其置为 obsolete 并移除相应实现代码。
| 对象状态 | Mark Word 可能承载的信息 | 边界 |
|---|---|---|
| 普通状态 | 哈希、年龄、状态位等 | 哈希可能尚未计算 |
| 轻量级同步状态 | 与锁记录或锁状态相关的数据 | 具体编码依 JDK 实现 |
| 重量级同步状态 | 与 ObjectMonitor 相关的数据 | 具体指针/索引形式可变化 |
| GC 处理状态 | 收集器所需的标记信息 | 取决于收集器与实现 |
因此可以用“Mark Word 是对象运行时状态栏”解释设计,但不应把旧版本的固定二进制标志或单向“偏向→轻量→重量”流程当成所有现代 JDK 的永久结论。
三、一道经典追问:一个 Object 占多少字节
在常见 64 位 HotSpot、启用压缩类指针且按 8 字节对象对齐的示例配置下:
new Object():若 Mark Word 为 8 字节、压缩类指针为 4 字节,则头部 12 字节,再填充到 8 的倍数,示例结果为 16 字节。- 带字段的对象:16 字节起 + 字段大小,再对齐到 8 的倍数。
指针压缩是 HotSpot 的重要优化:在可编码的堆布局下,对象引用可用较小值表示,从而降低引用和缓存占用。可压缩的堆上限受对象对齐、零基址映射和 JVM 决策影响,常见 32 GB 只是经验边界,不能据此武断限制堆大小。
四、字段重排与对齐
JVM 不一定按源码声明顺序摆放字段,而会在语义约束内采用实现相关的布局策略,以减少空隙并满足各字段的对齐要求。父类字段、字段分组以及 JVM 版本都会影响最后顺序,所以不能只按 long、int、boolean 的声明大小直接相加。对齐有利于地址计算和处理器访问,却也可能让小字段后的空洞变成额外开销。应在目标 JDK 与参数下使用 JOL(Java Object Layout) 查看真实偏移,而不是把某个示例布局当成规范。
五、用数字算一遍,但要声明前提
在常见 64 位 HotSpot、启用压缩类指针、对象按 8 字节对齐的某种配置下,可做如下示例:
class Sample {
int id; // 4 字节
boolean ok; // 1 字节
Object ref; // 压缩普通对象指针时常见 4 字节
}
| 组成 | 示例字节数 | 说明 |
|---|---|---|
| Mark Word | 8 | 具体位含义随 JVM/JDK 和锁状态变化 |
| Klass Pointer | 4 | 启用压缩类指针时的常见值 |
| 实例字段 | 9 | 字段可能被重排并插入空隙 |
| 对齐填充 | 视布局而定 | 使总大小满足对象对齐要求 |
简单相加是 21 字节,但实际布局可能重排为 24 字节或其他结果。必须用目标 JVM 的 JOL 输出确认,不能只凭声明顺序手算。
new Object() 常见为 16 字节,也只是特定 HotSpot 配置下的经验值;关闭压缩类指针、改变对象对齐或使用不同 JVM 都可能改变结果。
六、对象头会随运行时状态变化
Mark Word 是复用字段,可能编码哈希、GC 年龄、锁相关状态等信息。锁实现和对象头设计随 JDK 演进,不能把某个版本“偏向锁、线程 ID、锁指针”的位图当成永久标准。
压缩普通对象指针能否启用与最大堆、对象对齐、零基址映射等有关。“堆超过 32 GB 压缩指针必然关闭”只是某些典型配置下的粗略经验,阈值可能因对齐和 JVM 决策变化。应查看启动日志或实际 VM flags。
对象布局属于 JVM 实现细节。面试可以讲 HotSpot 常见结构,但要明确位数、压缩指针、对象对齐和 JDK 版本四个前提。
七、常见误区与追问
- 误区:Java 对象只占字段声明的大小。 对象还包含对象头、父类字段、字段间空隙和末尾对齐填充。
- 误区:所有 64 位 JVM 中
new Object()都固定为 16 字节。 这依赖 HotSpot 配置、压缩类指针和对齐参数。 - 误区:Mark Word 永久保存同一组固定字段。 它会按对象状态复用,其布局也随 JVM 版本变化。
- 追问:为什么需要对象对齐? 对齐可简化地址计算和内存访问,也为压缩指针编码提供条件,但会引入填充浪费。
- 追问:字段声明顺序等于内存顺序吗? 不一定;JVM 可能在满足继承和语义约束的前提下重排字段以减少空隙。
- 追问:数组对象比普通对象多什么? 对象头还需要保存数组长度,元素区之后同样可能有对齐填充。
- 追问:怎样得到线上 JVM 的真实布局? 在相同 JDK、参数和类定义下使用 JOL,并核对压缩指针与对象对齐配置。
八、加强记忆
用“头、数据、填充”记住 HotSpot 常见对象结构:对象头提供类型与运行时状态,实例数据保存字段,末尾填充满足对齐。Mark Word 会按状态复用,精确位图、偏向锁流程和锁编码都受 JDK 版本影响,不能当成 JVM 规范。new Object() 常见 16 字节以及压缩指针约 32 GB 的经验边界都必须附带位数、压缩类指针、对象对齐和堆布局前提。遇到具体大小题,先列前提再手算,最后用目标环境的 JOL 和 VM flags 验证。