对象内存布局

候选人小陈在面试拼多多 P6 时,面试官问道:

"Java 对象的内存布局由哪几部分组成?"

小陈说:"有对象头和实例数据..."面试官追问:"对象头包含什么?64 位 JVM 下对象头占多少字节?"

小陈答不上来。面试官继续追问:"为什么对象要按 8 字节对齐?"

小陈彻底卡住了...

一、核心问题:对象内存布局 🔴

1.1 问题拆解

第一层:组成部分(有哪些?)
  "对象内存布局由哪几部分组成?"
  考察点:对象头、实例数据、对齐填充

第二层:对象头结构(存什么?)
  "对象头包含哪些数据?Mark Word 存储了什么?"
  考察点:64 位 vs 32 位、Mark Word 字段复用

第三层:对齐填充(为什么?)
  "对象为什么要按 8 字节对齐?"
  考察点:缓存行、GC 效率

1.2 ❌ 错误示范

候选人原话 A:"对象头就是 Mark Word,占 8 字节。"

问题诊断:对象头包含两部分:Mark Word(8 字节)和 Klass Pointer(4 或 8 字节)。所以对象头不一定是 8 字节。

候选人原话 B:"对象的大小就是所有字段大小的总和。"

问题诊断:忽略了对象头(Mark Word + Klass Pointer)和对齐填充。两个 int 字段(各 4 字节)加起来是 8 字节,但加上对象头后就是 16 字节。

1.3 标准回答

P5 级别:对象布局组成

Java 对象的内存布局

[对象头 Object Header]
  [Mark Word: 8 bytes]
  [Klass Pointer: 4 bytes (开启压缩指针时) / 8 bytes (未开启)]
[实例数据 Instance Data]
  [父类字段 + 子类字段]
[对齐填充 Padding]
  [填充到 8 字节的倍数]

一个典型对象的大小计算

class Example {
    byte b1;       // 1 byte
    int i;        // 4 bytes
    byte b2;       // 1 byte
}

// 内存布局(压缩指针):
// 对象头: 8 (Mark Word) + 4 (Klass Pointer) + 4 (Padding) = 16
// 实例数据: b1(1) + padding(3) + i(4) + b2(1) + padding(3) = 12
// 总大小: 16 + 12 = 28 → 填充到 8 的倍数 = 32 bytes

P6 级别:Mark Word 与 Klass Pointer

Mark Word(8 字节 = 64 bits)的状态存储

锁状态Mark Word 内容(低到高)lock bits
无锁[hashcode:31][gc_age:4][biased:0][01]01
偏向锁[thread:54][epoch:2][gc_age:4][biased:1][01]01
轻量锁[ptr to Lock Record:62][00]00
重量锁[ptr to Monitor:62][10]10
GC 标记[__:62][11]11

Klass Pointer(类型指针)

  • 开启压缩指针(-XX:+UseCompressedOops,默认):4 字节,最大寻址 32GB 堆
  • 关闭压缩指针(-XX:-UseCompressedOops):8 字节
// 压缩指针的工作原理
// 存储时:地址右移 3 位(因为对象按 8 字节对齐,低 3 位为 0)
// 读取时:地址左移 3 位

P7 级别:对齐填充

为什么按 8 字节对齐?

  1. GC 效率:对象引用通常是 8 字节对齐的。如果对象起始地址是 8 的倍数,GC 遍历对象图时可以直接通过引用计算对象地址

  2. CPU 访问效率:现代 CPU 访问对齐地址的效率更高(不对齐访问可能触发 CPU 异常或多次内存访问)

  3. 减少填充开销:虽然对齐会浪费一些空间,但总体上减少了 GC 和 CPU 访问的开销

JOL(Java Object Layout)工具查看对象布局

import org.openjdk.jol.info.ClassLayout;

System.out.println(ClassLayout.parseInstance(new Example()).toPrintable());

// 输出:
// [B] @ offset 16
// [I] @ offset 20
// [B] @ offset 24
// ... 对象大小等信息

【面试官心理】 这道题我能问到 P7 级别,是因为对象内存布局涉及了 JVM 对象头设计、压缩指针、GC 算法等多个维度。能说出 Mark Word 字段复用的候选人说明他理解了 synchronized 的底层实现。

1.4 追问升级

追问:数组的对象布局和普通对象有什么区别?

数组的对象头多了 4 字节的数组长度字段

[Mark Word: 8 bytes]
[Klass Pointer: 4 bytes]
[Array Length: 4 bytes]  ← 数组特有
[Array Data: variable]
[Padding: to 8-byte boundary]

二、生产避坑 🟡

2.1 对象大小估算

// 快速估算对象大小
// 对象头 = 12/16 字节(取决于压缩指针)
// 每个字段 = 类型大小 + 填充
// 总大小 = 填充到 8 的倍数

// JDK 9+ 的 JOL
// <dependency>
//     <groupId>org.openjdk.jol</groupId>
//     <artifactId>jol-core</artifactId>
//     <version>0.16</version>
// </dependency>

2.2 伪共享导致的性能问题

当两个对象的字段位于同一缓存行时,修改一个字段会导致另一个字段的缓存行失效。使用 @Contended 注解( JDK 8+)或手动 padding 可以解决。

💡

面试加分点:能说出"JDK 21 的虚拟线程(Virtual Threads)的栈大小默认是 1MB,但可以动态增长,实际内存占用接近实际使用量而非预分配大小",说明他关注了 JDK 21 的新特性。

⚠️

面试陷阱:被问到"为什么压缩指针只能支持 32GB 堆",很多人会说"因为指针是 4 字节"。准确答案是:4 字节只能表示 2^32 个值,每个值乘以 8(因为对象按 8 字节对齐,低 3 位总是 0),最大可寻址 2^32 × 8 = 32GB。