对象布局
面试提问
"一个 Java 对象在内存中是如何存储的?对象头包含哪些信息?"
核心概念
在 HotSpot JVM 中,Java 对象在堆内存中的布局分为三部分:
┌─────────────────────────────────────────────┐
│ 对象头(Object Header) │
│ ├─ Mark Word(8 字节) │
│ └─ 类型指针(4/8 字节) │
├─────────────────────────────────────────────┤
│ 实例数据(Instance Data) │
│ └─ 字段数据(各类型占用不同) │
├─────────────────────────────────────────────┤
│ 对齐填充(Padding) │
│ └─ 保证对象大小是 8 字节的倍数 │
└─────────────────────────────────────────────┘对象头详解
1. Mark Word(标记字)
Mark Word 是对象头的核心,大小为 8 字节(64 位 JVM),存储对象运行时信息。
结构(64 位 JVM,开启指针压缩):
┌─────────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├─────────────────────────────────────────────────────────────┤
│ 无锁状态: │
│ │ unused:25 │ hashcode:31 │ unused:1 │ age:4 │ biased:1 │ lock:2 │
├─────────────────────────────────────────────────────────────┤
│ 偏向锁状态: │
│ │ thread:54 │ epoch:2 │ unused:1 │ age:4 │ biased:1 │ lock:2 │
├─────────────────────────────────────────────────────────────┤
│ 轻量级锁状态: │
│ │ ptr_to_lock_record:62 │ lock:2 │ │
├─────────────────────────────────────────────────────────────┤
│ 重量级锁状态: │
│ │ ptr_to_heavyweight_monitor:62 │ lock:2 │ │
├─────────────────────────────────────────────────────────────┤
│ GC 标记状态: │
│ │ unused:62 │ lock:2 │ (11) │
└─────────────────────────────────────────────────────────────┘关键字段说明:
| 字段 | 大小 | 说明 |
|---|---|---|
hashcode | 31 bits | 对象哈希码(延迟计算) |
age | 4 bits | 对象年龄(GC 分代年龄,最大 15) |
biased | 1 bit | 偏向锁标志 |
lock | 2 bits | 锁状态(01 无锁/偏向,00 轻量级,10 重量级,11 GC 标记) |
thread | 54 bits | 偏向线程 ID |
epoch | 2 bits | 偏向时间戳 |
锁状态对应表:
| lock | biased | 状态 |
|---|---|---|
| 01 | 0 | 无锁 |
| 01 | 1 | 偏向锁 |
| 00 | - | 轻量级锁 |
| 10 | - | 重量级锁 |
| 11 | - | GC 标记 |
2. 类型指针(Class Pointer)
指向方法区中对象的类元数据,用于确定对象是哪个类的实例。
- 大小:4 字节(开启指针压缩)或 8 字节(未开启)
- 数组对象额外有 4 字节存储数组长度
# 开启指针压缩(JDK8 默认开启,堆 < 32G 时有效)
-XX:+UseCompressedOops
# 压缩类指针
-XX:+UseCompressedClassPointers实例数据
存储对象中定义的字段数据,包括父类继承的字段。
字段类型占用空间:
| 类型 | 占用空间 |
|---|---|
boolean | 1 字节 |
byte | 1 字节 |
char | 2 字节 |
short | 2 字节 |
int / float | 4 字节 |
long / double | 8 字节 |
| 引用类型 | 4 字节(压缩指针)/ 8 字节 |
字段重排序: JVM 会对字段进行重排序,让长短不一的字段紧凑排列,减少内存占用。
示例:
public class Example {
boolean b; // 1 字节
int i; // 4 字节
long l; // 8 字节
char c; // 2 字节
}内存布局可能是:long(8) + int(4) + char(2) + boolean(1) + padding(1) = 16 字节
对齐填充
确保对象大小是 8 字节的倍数,便于内存管理和 GC。
原因:
- 内存对齐,提高访问效率
- 方便计算对象边界
对象大小计算示例
示例一:空对象
public class Empty {}内存布局:
Mark Word: 8 字节
Class Pointer: 4 字节(压缩指针)
Padding: 4 字节
─────────────────────
总计: 16 字节示例二:简单对象
public class Simple {
int id; // 4 字节
boolean flag; // 1 字节
}内存布局:
Mark Word: 8 字节
Class Pointer: 4 字节
id: 4 字节
flag: 1 字节
Padding: 3 字节
─────────────────────
总计: 20 字节 → 对齐后 24 字节示例三:数组对象
int[] arr = new int[10];内存布局:
Mark Word: 8 字节
Class Pointer: 4 字节
Array Length: 4 字节(数组特有)
Data: 10 * 4 = 40 字节
Padding: 4 字节
─────────────────────
总计: 60 字节 → 对齐后 64 字节指针压缩
为什么需要指针压缩?
64 位 JVM 中,指针占用 8 字节,比 32 位多一倍,导致:
- 对象变大,内存占用增加
- CPU 缓存命中率降低
压缩原理
将 64 位指针压缩为 32 位,通过位移还原:
压缩地址 = 实际地址 / 8
实际地址 = 压缩地址 * 8限制:只能寻址 4G * 8 = 32G 内存
开启条件
# JDK8 默认开启,堆 < 32G 时有效
-XX:+UseCompressedOops # 压缩普通对象指针
-XX:+UseCompressedClassPointers # 压缩类指针堆 >= 32G 时,指针压缩自动失效。
对象大小查看工具
1. JOL(Java Object Layout)
添加依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>使用示例:
import org.openjdk.jol.info.ClassLayout;
public class JolDemo {
public static void main(String[] args) {
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) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000851
12 4 (object alignment gap)
Instance size: 16 bytes2. Instrumentation
import java.lang.instrument.Instrumentation;
public class ObjectSizeUtil {
private static Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
instrumentation = inst;
}
public static long sizeOf(Object obj) {
return instrumentation.getObjectSize(obj);
}
}面试要点总结
| 问题 | 答案要点 |
|---|---|
| 对象布局三部分? | 对象头、实例数据、对齐填充 |
| Mark Word 存储什么? | 哈希码、分代年龄、锁状态标志、偏向线程 ID |
| 为什么对象大小要 8 字节对齐? | 内存对齐提高访问效率,方便 GC 管理 |
| 指针压缩原理? | 64 位指针压缩为 32 位,通过位移还原,最多寻址 32G |
| 对象年龄最大是多少? | 15(4 bits,最大值 2^4 - 1 = 15) |
相关题目
- 计算一个对象占用的内存大小?
- 为什么对象头中的分代年龄最大是 15?
- 指针压缩有什么限制?
- 数组对象和普通对象的内存布局有什么区别?
参考资料
- JOL - Java Object Layout (opens in a new tab)
- 《深入理解 Java 虚拟机》- 周志明
- Compressed OOPs (opens in a new tab)