Java对象在内存中的布局详解
1、Java 对象内存布局(HotSpot 虚拟机)
在 HotSpot 虚拟机 中,一个 Java 对象在堆内存中的存储布局可以分为以下几个部分:
1、对象头(Object Header)
对象头是对象内存布局中最重要的部分之一,它存储了关于对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态等。
对象头又分为两部分(在 64 位 JVM 中):
组成部分 | 说明 |
---|---|
Mark Word(标记字段) | 存储对象自身的运行时数据,如:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID等 |
Klass Pointer(类型指针) | 指向对象所属类的元数据(即 Class 对象)的指针,JVM 通过它确定该对象是哪个类的实例 |
注意:
1、对象头是实现 synchronized、hashCode、垃圾回收、锁升级等机制的基础!
2、如果对象是一个 数组,对象头中还会多一个部分:数组长度(Array Length)
1、Mark Word(标记字段)详解(以 64 位 JVM 为例)
Mark Word 是对象头的一部分,它是一个 动态的数据结构,根据对象当前的状态(是否偏向锁、轻量级锁、重量级锁等),存储的内容会动态变化。
锁状态 | 存储内容简述 |
---|---|
无锁状态 | 对象的 hashCode、GC 分代年龄 |
偏向锁 | 偏向线程 ID、偏向时间戳、对象分代年龄 |
轻量级锁 | 指向栈中锁记录的指针 |
重量级锁 | 指向 Monitor(管程/互斥锁)的指针 |
GC 标记 | 与垃圾回收相关的标记(如三色标记) |
Mark Word 利用了 位运算与掩码,在一个 64-bit 的长整型中存储多种信息,非常精巧高效。
2、Klass Pointer(类型指针)
是一个指针,指向该对象所属的 类元数据(即 .class 对象),JVM 通过这个指针判断该对象是哪个类的实例。
在开启压缩指针(Compressed OOPs,默认开启)的情况下,占 4 字节;未开启则占 8 字节。
通过 Klass Pointer,JVM 知道这个对象是哪个 Class 的实例,从而可以找到方法、字段、父类等信息。
2、实例数据(Instance Data)
这是对象真正存储的 有效信息,也就是代码中定义的各种类型的字段(成员变量),包括:
1、从父类继承的字段
2、当前类中定义的字段
3、不同数据类型的字段(如 int、long、引用类型等)
实例数据部分是对象“业务数据”的存储区域,它的排列顺序受 JVM 实现与字段定义顺序影响,也可能被优化(如字段重排序)。
JVM 在布局时,可能会按照以下原则排列实例数据:
- 相同大小的字段尽量放在一起
- 父类字段在前,子类字段在后
- 可能进行 字段重排序以优化内存对齐
3. 对齐填充(Padding)
对齐填充不是必须的,但通常存在。
HotSpot 虚拟机要求 对象的大小必须是 8 字节的整数倍(即对象总大小对齐到 8 字节),这是为了提高内存访问效率(CPU 读取内存通常是按 8 字节对齐的)。
如果对象头 + 实例数据的总大小不是 8 的倍数,JVM 会在对象末尾填充一些 无意义的空白字节(Padding),以达到对齐的目的。
对齐填充 不存储任何有用信息,仅仅是为了内存布局优化。
2、Java 对象内存布局总结(图示)
下面是 一个普通 Java 对象在内存中的布局(64 位 JVM,开启压缩指针)的简化图示:
+---------------------------+
| 对象头 (Header) |
| - Mark Word (8字节) | --> 哈希、GC年龄、锁状态等
| - Klass Pointer (4字节) | --> 指向 Class 元数据
| | (如果是数组,还会有数组长度字段 4字节)
+---------------------------+
| 实例数据 (Fields) | --> 你定义的成员变量(int, 引用等)
+---------------------------+
| 对齐填充 (Padding) | --> 保证总大小是 8 字节倍数(可能没有)
+---------------------------+
3、对象大小估算(示例)
估算一个 Java 对象在内存中占用的空间大小示例
public class User{int id; // 4 字节String name; // 4 字节(引用类型,压缩指针下)boolean male; // 1 字节int age; // 4 字节
}
注意:这里说的是对象自身占用的内存,不包括它引用的对象(比如 name 是 String,String 对象在堆上另存)
对象布局分析(64 位 JVM,开启压缩指针):
成部分 | 大小(字节) | 说明 |
---|---|---|
对象头 | 12 字节 | Mark Word(8) + Klass Pointer(4) |
实例数据 | 4 (int id) + 4 (引用 name) + 1 (boolean male) + 4 (int age) = 13 字节 | |
对齐填充 | 3 字节 | 总计 12 + 13 = 25,不是 8 的倍数,填充到 28 字节(32 - 25 = 3) |
大约占用 28 字节(实际可能略有差异,依赖 JVM 实现)
4、特殊对象:数组对象的内存布局
如果对象是一个 数组,比如 int[]
、String[]
,那么对象头中会多一个字段:数组长度(Array Length,4 字节)
+---------------------------+
| 对象头 |
| - Mark Word (8字节) |
| - Klass Pointer (4字节) |
| - 数组长度 (4字节) | <-- 仅数组对象有
+---------------------------+
| 数组元素数据 | --> 比如 int[] 就是连续的 int 值
+---------------------------+
| 对齐填充 (如有) |
+---------------------------+
数组对象比普通对象多存储一个长度信息,占用额外 4 字节(压缩指针下)。
2、总结:
组成部分 | 说明 | 是否必有 | 大小(64位,压缩指针) |
---|---|---|---|
对象头(Header) | 包括 Mark Word 和 Klass Pointer | ✅ 必有 | 通常 12 字节(8 + 4) |
Mark Word | 存储哈希、GC 年龄、锁状态等动态信息 | ✅ 是对象头的一部分 | 8 字节 |
Klass Pointer | 指向该对象的类元数据(Class) | ✅ 是对象头的一部分 | 4 字节(可开启/关闭压缩) |
数组长度(仅数组) | 数组对象才有,表示数组长度 | ❌ 仅数组对象 | 4 字节 |
实例数据(Fields) | 对象的成员变量(包括继承的字段) | ✅ 必有 | 依据字段类型而定 |
对齐填充(Padding) | 保证对象总大小是 8 字节对齐 | ❌ 可能没有 | 0~7 字节 |
3、补充:
主题 | 说明 |
---|---|
synchronized 的实现 | 依赖对象头中的 Mark Word 实现锁状态记录 |
hashCode() 的默认实现 | 默认与对象头中的 Mark Word 相关(可重写) |
垃圾回收与对象年龄 | 对象头中存储 GC 分代年龄,用于判断是否进入老年代 |
锁升级(偏向锁、轻量级锁、重量级锁) | 基于对象头 Mark Word 中的状态标志实现 |
对象内存大小查看工具 | 如 JOL(Java Object Layout),可以精确查看对象布局与大小 |
Java 对象在内存中的布局分为对象头(包括 Mark Word 和 Klass Pointer)、实例数据(成员变量)、对齐填充三部分,其中对象头是实现锁、GC、哈希等机制的核心,对象大小受字段类型、对齐规则等影响。