Java对象底层存储结构详解
Java 对象底层存储结构
在 JVM 里,一个 Java 对象到底长什么样?你的 class 文件、字段、继承关系,最后在堆里是怎么被组织的?这一篇文章用尽量平实的语言、清晰的结构,把 HotSpot 中对象的真实面貌完整讲清。
1. Java 对象在内存中的三块内容
无论什么对象,它在堆里都由 三部分 组成:
[ 对象头 (Object Header) ]
[ 实例数据 (Instance Data) ]
[ 对齐填充 (Padding) ]简单理解:
- 对象头:存元信息(如锁状态、hashCode、类型指针等)
- 实例数据:类里面的字段真正存储的地方
- 对齐填充:为了让对象的大小变成 8 字节的整数倍,避免 CPU 访问异常
2. 对象头到底是什么?
对象头(Object Header)是最关键的部分,由两部分组成:
(1) Mark Word(运行时标记)
一个对象在 JVM 运行过程中会经历各种状态,比如:
- 是否被加锁
- 锁是什么类型
- 对象的 hashCode(如果计算过)
- GC 年龄
这些全部都放在 Mark Word 里。
Mark Word 常见大小:8 字节(64 位 JVM)
注意:Mark Word 不是固定内容,是动态变化的。
比如对象被 synchronized 加锁时,Mark Word 会替换成锁记录;对象被偏向锁时,又是另一种格式。
(2) Klass Pointer(类型指针)
对象必须知道自己“是哪个类”。
Klass Pointer 是一个指针,指向类的元数据(HotSpot 里的 Klass)。
大小:
- 32 位 JVM:4 字节
- 64 位 JVM(开启压缩指针):4 字节
- 64 位 JVM(未开启压缩指针):8 字节
大部分服务器 JVM 都是“64 位 + 压缩 oops”,所以 klass pointer 一般是 4 字节。
压缩指针(Compressed Oops)
核心原理:64位JVM用32位指针访问对象,节省内存
实现机制:
- 对象8字节对齐 → 地址低3位恒为0 (因为8的二进制: 1000)
- 32位存偏移量,真实地址 = 偏移量 << 3
- 引用从8字节→4字节,对象头从16字节→12字节
使用规则:
- 默认开启(堆 < 32GB)
- 自动关闭(堆 > 32GB)→ 最大寻址空间 = (2^32 个 offset) × 8 字节
相关参数:
-XX:+UseCompressedOops # 开启(默认)
-XX:-UseCompressedOops # 关闭
//Klass Pointer 之所以能压缩,是因为 Klass 自身(类元数据)的地址 HotSpot 也让它按 8 字节对齐,即Klass Pointer 在内存中存储时也是 8 字节对齐,确保偏移量 ×8 能正确寻址
-XX:+UseCompressedClassPointers # 压缩Klass指针3. 实例数据(Instance Data):字段如何排布?
实例数据就是类中的字段。
你在 class 写的是:
class User {
int id;
boolean vip;
long score;
}但最终在内存里,字段顺序 不一定跟源码一致,因为 JVM 会做字段重排(Field Reordering),目标是:
- 减少空间浪费
- 让字段对齐得更合理
- 优化 CPU 访问
基本规则:
- 父类字段在前,子类字段在后
- 大字段(long/double)优先
- 同类型字段尽量靠一起
- 引用类型会放在自己的分组里
JVM 会努力让 padding(填充字节)尽量少。
4. 数组对象的特殊结构
数组对象和普通对象不同。
数组多了一个 length(4 字节) 字段,并放在头部之后。
结构:
[ Mark Word ]
[ Klass Pointer ]
[ length ]
[ element0 ]
[ element1 ]
...注意:数组元素是连续存放的,这也是数组访问比 ArrayList 更快的原因之一。
5. 对齐与填充:为什么对象大小总是 8 的倍数?
HotSpot 要求对象大小必须是 8 字节对齐。
比如对象总大小是 22 字节
→ JVM 会补齐到 24 字节。
原因:
- 因为 64 位 CPU 在访问内存时要求或偏好按 8 字节对齐,否则会产生跨界访问,导致性能下降甚至硬件异常。
几乎所有对象结尾都有 padding。
6. 示例:一个对象在内存中的真实样子
假设类:
class User {
int id; // 4 字节
boolean vip; // 1 字节
long score; // 8 字节
byte level; // 1 字节
}JVM 可能排成这样:
[ Mark Word ] 8 bytes
[ Klass Ptr ] 4 bytes
// 对象头不需要额外对齐,只有对象本体必须最终对齐
// 对象头 = 12 bytes,字段从 offset = 12 开始
-----------------------------------------
// ✔ “字段”必须按照自己的天然对齐要求放置
// 比如 long/double 需要 8 对齐 ,int需要 4 对齐。
[ padding ] 4 <-- 这里的 padding 是“score 字段需要的对齐”
[ score ] 8 bytes
[ id ] 4 bytes
[ vip ] 1 byte
[ level ] 1 byte
[ padding ] 2 bytes // 对齐到 8 的倍数
-----------------------------------------
总计:32 字节
//如果不优化, 从offset = 12 开始
[ id ] 4 bytes // 已经是 4 的倍数,无需对齐
[ vip ] 1 byte
[ padding ] 7 bytes // 为了让 long 达到 8 字节对齐
[ score ] 8 bytes
[ level ] 1 byte // offset = 33
[ padding ] 7 bytes
-----------------------------------------
总计: 12 (头) + 4 + 1 + 7 + 8 + 1 + 8 = 40 字节你会发现字段顺序和源码不一样,这正是 JVM 的优化。
7. 总结
Java 对象在内存里由三部分组成:对象头、实例数据和对齐填充。对象头包含 Mark Word(Hash、锁、GC 信息)和类型指针(指向类元数据)。字段的排布不会严格按照源码顺序,而是由 JVM 按类型、父类等规则重排,以减少对齐开销。对象总大小必须是 8 字节对齐。数组对象比普通对象多一个 length 字段。
一句话总结:
Java 对象结构 = 16 字节对象头 + 字段(重排) + 填充到 8 字节倍数。