对象内存布局
开篇:一个 Java 对象在内存里长什么样?
写了几年 Java,你可能 new 过成千上万个对象,但有没有想过:new Object() 在堆内存中到底占多少字节?一个只有一个 int 字段的类,和一个空对象差多少?
这些问题的答案,直接影响到你对锁升级、GC 年龄、对象头压缩等知识的理解。所以在深入 synchronized 锁升级之前,我们先把"对象在内存里长什么样"这件事搞清楚。
一、对象的三部分
在 HotSpot 虚拟机中,一个对象在堆内存中由三部分组成:
┌─────────────────────────────────────────┐
│ 对象头 (Header) │
│ ┌──────────────────────────────────┐ │
│ │ Mark Word (标记字段) │ │
│ │ 8 bytes (64位VM) │ │
│ ├──────────────────────────────────┤ │
│ │ Klass Pointer (类型指针) │ │
│ │ 4 bytes (压缩) / 8 bytes (不压缩)│ │
│ └──────────────────────────────────┘ │
├─────────────────────────────────────────┤
│ 实例数据 (Instance Data) │
│ 存放类中定义的字段(含父类字段) │
├─────────────────────────────────────────┤
│ 对齐填充 (Padding) │
│ 补齐到 8 字节的整数倍 │
└─────────────────────────────────────────┘1.1 对象头
对象头分为两部分:
Mark Word(标记字段):存储对象自身的运行时数据,包括 hashCode、GC 分代年龄、锁状态标志、持有锁的线程 ID 等。在 64 位虚拟机中占 8 个字节。
Mark Word 被设计成非固定的数据结构,根据对象当前的状态复用存储空间。随着锁状态的变化,里面存储的内容也不一样:
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | hashCode(31) + GC分代年龄(4) + 偏向锁标志(1) | 01 |
| 偏向锁 | 线程ID(54) + Epoch(2) + GC分代年龄(4) + 偏向锁标志(1) | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
| GC 标记 | 空 | 11 |
这张表是理解 synchronized 锁升级的关键。无锁和偏向锁都是 01,通过偏向锁标志位来区分。
Klass Pointer(类型指针):指向方法区中该对象所属类的元数据(Class 信息),虚拟机通过它确定"这个对象是哪个类的实例"。开启指针压缩(默认开启)时占 4 字节,不压缩占 8 字节。
如果对象是数组,对象头中还会额外存储数组长度,占 4 个字节。
1.2 实例数据
存放类中定义的所有字段(包括从父类继承的字段)。各字段的排列顺序受到虚拟机分配策略(FieldsAllocationStyle)和字段在源码中定义顺序的共同影响。HotSpot 默认策略是相同宽度的字段会被分配在一起,以减少内存空洞。
1.3 对齐填充
HotSpot 要求对象的起始地址必须是 8 字节的整数倍,不是必须存在的部分,仅起"凑齐"的作用。
为什么是 8 字节?这是一种空间换时间的权衡。CPU 按 cache line(通常 64 字节)读取内存,8 字节对齐可以让对象不跨越 cache line 边界,减少一次内存访问变成两次的情况,同时也简化了指针压缩的实现(4 字节指针左移 3 位可以寻址 32GB 空间)。
二、用 JOL 工具实战验证
纸上得来终觉浅。JOL(Java Object Layout)是 OpenJDK 官方提供的对象布局分析工具,用它一看便知。
2.1 添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>2.2 查看空对象
public class JolDemo {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}典型输出(64 位、开启压缩指针):
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) Mark Word 低4字节
4 4 (object header) Mark Word 高4字节
8 4 (object header) Klass Pointer (压缩后)
12 4 (loss due to alignment) 对齐填充
Instance size: 16 bytes- Mark Word: 8 字节
- Klass Pointer: 4 字节(压缩)
- 实例数据: 0 字节(Object 没有字段)
- 对齐填充: 4 字节(凑到 16 = 8 的倍数)
空对象就占了 16 字节。
2.3 查看有字段的对象
class MyObj {
private int i; // 4 bytes
private boolean flag; // 1 byte
private double d; // 8 bytes
}
System.out.println(ClassLayout.parseInstance(new MyObj()).toPrintable());典型输出:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header) Mark Word 低4
4 4 (object header) Mark Word 高4
8 4 (object header) Klass Pointer
12 4 int MyObj.i
16 8 double MyObj.d
24 1 boolean MyObj.flag
25 7 (loss due to alignment)
Instance size: 32 bytes注意 double 被提前了,boolean 被排到最后,这就是 HotSpot 的字段重排策略:相同宽度的字段放在一起,大宽度字段优先对齐,以减少内存空洞。
2.4 查看锁状态变化
JOL 最强大的用途之一是观察锁升级。通过打印同一个对象在加锁前后的 Mark Word,可以直观看到锁标志位的变化:
Object lock = new Object();
System.out.println("无锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
System.out.println("加锁后:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}在 Mark Word 的最低两位,你能看到从 01(无锁/偏向锁)变成 00(轻量级锁)的过程。这也印证了前面那张锁状态表。
三、关键问题 FAQ
Q:关闭指针压缩后,空对象占多少字节?
关闭后 Klass Pointer 从 4 字节变成 8 字节,加上 Mark Word 8 字节,共 16 字节,刚好对齐,无需填充。用 -XX:-UseCompressedOops 关闭。
Q:GC 分代年龄为什么最大是 15?
因为 Mark Word 中只给分代年龄留了 4 bit,最大值就是 1111 = 15。所以 -XX:MaxTenuringThreshold 不能超过 15。
Q:对象头和 synchronized 有什么关系?
synchronized 的锁信息就存在 Mark Word 里。无锁、偏向锁、轻量级锁、重量级锁四种状态的升级,本质就是 Mark Word 中存储内容的变化。所以理解对象头,是理解锁升级的前提。
Q:为什么 hashCode 和偏向锁不能共存?
看上面的表就能发现:偏向锁状态下 Mark Word 存储的是线程 ID,没有空间放 hashCode。一旦调用了对象的 hashCode() 方法,偏向锁就会被撤销。这也是偏向锁在某些场景下"退化"的原因之一。
小结
| 部分 | 内容 | 大小(64位、压缩指针) |
|---|---|---|
| Mark Word | hashCode / GC年龄 / 锁状态 | 8 字节 |
| Klass Pointer | 指向类元数据 | 4 字节 |
| 实例数据 | 类中定义的字段 | 按实际字段 |
| 对齐填充 | 凑到 8 字节的倍数 | 0~7 字节 |
一个空的 new Object() 占 16 字节。每个对象至少有 12 字节的"税"(对象头),这也是为什么在对象数量极多的场景下,推荐使用基本类型数组而非对象数组来节省内存。
掌握了对象内存布局,你就有了理解 synchronized 锁升级、GC 年龄阈值、指针压缩等高级话题的坚实基础。