对象内存布局与访问定位
对象在堆里长什么样
对象在堆中不是「随意摆放」的。JVM 对对象的内存布局有严格的规定,理解它有助于理解对象头、锁优化、以及如何排查内存问题。
对象的内存布局
HotSpot 中,对象在堆中的内存布局分为三部分:
┌──────────────────────────────────────────────────┐
│ 对象内存布局 │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 对象头(Header) │ │
│ │ │ │
│ │ ┌────────────────┐ ┌─────────────────┐ │ │
│ │ │ Mark Word │ │ 类型指针(klass) │ │ │
│ │ │ (8 bytes) │ │ (4/8 bytes) │ │ │
│ │ │ 哈希码/GC分代/锁 │ │ 指向类元数据 │ │ │
│ │ └────────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 实例数据(Instance Data) │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌────────────────┐ │ │
│ │ │ name (引用) │ │ age (int) │ │ │
│ │ │ 4/8 bytes │ │ 4 bytes │ │ │
│ │ └──────────────────┘ └────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 对齐填充(Padding) │ │
│ │ 保证对象大小是 8 的倍数 │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘对象头(Object Header)
对象头是对象在内存布局中最核心的部分。HotSpot 的对象头分两部分:
1. Mark Word(8 字节 / 64位系统)
Mark Word 存储了对象的运行时数据,高度压缩:
| 状态 | Mark Word 内容(64 位) |
|---|---|
| 无锁状态 | 对象哈希码(25位) + GC分代年龄(4位) + 偏向锁标志(1位) + 锁标志位(4位) |
| 轻量级锁 | 指向栈中锁记录的指针(62位) + 锁标志位 |
| 重量级锁 | 指向互斥量(重量级锁)的指针(62位) + 锁标志位 |
| GC 标记 | 空(62位) + 锁标志位 |
| 偏向锁 | 线程 ID(54位) + Epoch(2位) + 分代年龄(4位) + 偏向锁标志(1位) + 锁标志位 |
Mark Word 的大小是固定的 8 字节,但内容会根据对象状态动态变化——这是 JVM 空间换时间的经典设计。
2. 类型指针(Klass Pointer)
指向方法区中类元数据的指针。
| 系统 | 类型指针大小 | 说明 |
|---|---|---|
| 32 位系统 | 4 字节 | |
| 64 位系统(默认) | 8 字节 | |
| 64 位系统 + 压缩指针 | 4 字节 | -XX:+UseCompressedClassPointers |
对象大小计算
java
public class ObjectLayout {
static class A {
int a; // 4 字节
}
static class B {
int a; // 4 字节
int b; // 4 字节
}
static class C {
int a; // 4 字节
long b; // 8 字节(8 字节对齐)
int c; // 4 字节
}
static class D {
Object o; // 引用:4 字节(开启压缩指针)
}
static class E {
int i; // 4 字节
Object o; // 引用:4 字节
double d; // 8 字节
}
}用 JOL 查看实际布局
java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
// 需要添加依赖:org.openjdk.jol:jol-core
public class JOLDemo {
public static void main(String[] args) {
System.out.println(VM.current().details());
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}输出示例:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; lasts: 0)
8 4 (object header: class) 0x0000000000000001
12 4 (object alignment/padding) (loss due to the next object alignment)
Instance size: 16 bytes对象访问定位:如何找到堆中的对象
Java 程序通过引用访问对象。引用存储在虚拟机栈的局部变量表中。但引用本身只是一个地址,JVM 如何通过引用找到堆中的对象?
有两种主流方式:
方式一:句柄访问(早期使用)
┌─────────────────────────────────────────────┐
│ 虚拟机栈 │ 局部变量表 │
│ ┌────────┐ ┌─────────┐ │
│ │ 引用 │→│ 句柄地址 │ │
│ └────────┘ └────┬────┘ │
└────────────────────┼─────────────────────────┘
│
▼
┌──────────────────┐
│ 句柄池 │
│ │
│ ┌──────────────┐ │
│ │ 对象实例地址 │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ 类型数据指针 │ │
│ └──────────────┘ │
└──────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 对象实例 │ │ 类元数据 │
│ (堆) │ │(方法区) │
└──────────┘ └──────────┘优点:对象被 GC 移动时(复制算法整理内存),只需修改句柄池中的指针,引用本身不变。
方式二:直接指针访问(HotSpot 使用)
┌─────────────────────────────────────────────┐
│ 虚拟机栈 │ 局部变量表 │
│ ┌────────┐ │
│ │ 引用 │───────────────────────────┐ │
│ └────────┘ │ │
└────────────────────────────────────────┼──────┘
│
▼
┌──────────────────────────────────┐
│ 对象头 │ 实例数据 │
│ Mark Word │ 类型指针 │ │
└────────────┴──────────┴───────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 类元数据 │ │ 对象字段 │
│(方法区) │ │ (堆) │
└──────────┘ └──────────┘优点:访问速度快(少一次指针跳转)。HotSpot 采用这种方式。
对齐填充的影响
HotSpot 的对象起始地址必须是 8 字节的倍数。不足的部分用空字节填充。
java
public class PaddingDemo {
static class Small {
int a; // 4 字节
// 需要 4 字节 padding 才能对齐
}
static class Optimal {
int a; // 4 字节
int b; // 4 字节
// 刚好 8 字节,无 padding
}
static class Worst {
int a; // 4 字节
int b; // 4 字节
int c; // 4 字节
int d; // 4 字节
int e; // 4 字节
// 20 字节 → padding 4 字节 → 24 字节
}
}padding 的实战影响
字段的声明顺序会影响对象大小:
java
public class FieldOrder {
// 方式一:浪费空间
static class BadOrder {
long a; // 8 字节
int b; // 4 字节
int c; // 4 字节
// padding 4 字节
// 总计:24 字节
}
// 方式二:节省空间
static class GoodOrder {
int b; // 4 字节
int c; // 4 字节
long a; // 8 字节
// 无 padding
// 总计:16 字节
}
}所以,把大字段放在前面,小字段放在后面,可以减少 padding。
本节小结
对象的内存布局:
| 部分 | 大小 | 说明 |
|---|---|---|
| Mark Word | 8 字节(64位) | 存储哈希码、GC 分代年龄、锁信息 |
| 类型指针 | 4/8 字节 | 指向方法区的类元数据 |
| 实例数据 | 按字段类型 | 对象字段的实际内容 |
| 对齐填充 | 0~7 字节 | 保证对象大小是 8 的倍数 |
HotSpot 的对象访问通过直接指针,引用直接指向对象,跳过句柄池,访问速度更快。
理解对象布局,有助于理解偏向锁、轻量级锁的底层实现,以及为什么字段顺序会影响对象大小。
到这里,「JVM 运行时数据区」全部章节都已完成。
