对象内存布局
zhuib 2020/12/11 JVM
# 对象内存布局
# Java 对象内存布局详解
# 1. 概述
在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为三个部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
# 2. 各部分详解
# 2.1 对象头(Header)
对象头是对象在内存中最重要的部分,它包含了对象的元数据信息,例如对象的类型、状态、锁信息等。 对象头可以分为两部分:
- Mark Word
- 类型指针
# 2.1.1 Mark Word
作用: 存储对象自身的运行时数据,例如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等信息。
存储结构: Mark Word 的长度在 32 位虚拟机中是 4 字节,在 64 位虚拟机中是 8 字节。 其存储结构会随着对象状态的不同而发生变化。 例如,在 32 位的 HotSpot 虚拟机中,Mark Word 的结构如下:
状态 标志位 内容 未锁定 01 对象哈希码、对象分代年龄 偏向锁 01 偏向线程 ID、偏向时间戳、对象分代年龄 轻量级锁 00 指向栈中锁记录的指针 重量级锁 10 指向 Monitor 对象的指针 GC 标记 11 空 可偏向 01 1:可偏向,0:不可偏向 偏向锁 (Biased Locking): 一种锁优化技术,用于减少无竞争的锁获取操作的开销。 当一个线程第一次访问一个对象并获取锁时,会在对象头的 Mark Word 中记录下该线程的 ID。 以后该线程再次访问该对象时,无需再进行锁获取操作,直接判断 Mark Word 中是否存储着自己的线程 ID 即可。 只有当有其他线程尝试获取该锁时,才会进行锁撤销操作。
# 2.1.2 类型指针
- 作用: 指向该对象的类元数据(Klass)的指针。 JVM 通过这个指针来确定该对象是哪个类的实例。
- 长度: 类型指针的长度在 32 位虚拟机中是 4 字节,在 64 位虚拟机中是 8 字节。 但是,如果开启了指针压缩(UseCompressedOops),那么类型指针的长度会缩短为 4 字节。
- 数组对象: 如果对象是一个数组,那么对象头中还会包含一个额外的字段,用于记录数组的长度。
# 2.2 实例数据(Instance Data)
- 作用: 存储对象真正的有效信息,也就是程序代码中所定义的各种字段的值,包括父类继承下来的和自己定义的。
- 存储顺序: 相同宽度的字段会被分配到一起,这样可以减少对齐填充的开销。 在 HotSpot 虚拟机中,字段的分配顺序如下:
longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,即对象引用)。 - 对齐规则: 如果开启了指针压缩(UseCompressedOops),那么所有字段都会按照 8 字节对齐。 如果没有开启指针压缩,那么对象引用类型字段会按照 4 字节对齐。
# 2.3 对齐填充(Padding)
- 作用: 起到占位符的作用。 由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,因此当对象实例数据部分没有对齐时,需要通过对齐填充来补齐。
- 长度: 对齐填充的长度不确定,取决于实例数据是否对齐。
# 3. 示例
假设有一个类定义如下:
class MyObject {
int i;
long l;
boolean b;
Object obj;
}
# 4. 对象访问方式
Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。 对象的访问方式取决于 JVM 虚拟机的实现,目前主流的访问方式有两种:
- 句柄访问
- 直接指针访问
# 4.1 句柄访问
- 实现: 在堆中划分出一块内存来作为句柄池,reference 中存储的是对象的句柄地址。 句柄中包含了对象实例数据和类型数据各自的具体地址信息。
- 优点: reference 中存储的是稳定的句柄地址,在对象被移动(例如垃圾回收)时,只需要改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 缺点: 需要额外的句柄池空间,并且访问对象时需要进行两次指针定位。
# 4.2 直接指针访问
- 实现: reference 中存储的是对象的直接地址。
- 优点: 访问对象时只需要进行一次指针定位,速度更快。
- 缺点: 如果对象被移动(例如垃圾回收),那么需要修改 reference 本身的值。
HotSpot 虚拟机采用的是直接指针访问的方式。
# 5. 总结
理解 Java 对象的内存布局对于理解 JVM 的底层机制、进行性能调优至关重要。 掌握对象头、实例数据和对齐填充的含义,以及对象访问方式,有助于我们更好地编写高效、稳定的 Java 程序。