Java 对象在内存中的物理布局(对象头、实例数据、对齐填充)
在 Java 虚拟机(以最常用的 HotSpot JVM 为例)中,一个 Java 对象在堆内存中的物理布局主要分为三个部分:对象头(Object Header)、实例数据(Instance Data) 和 对齐填充(Alignment Padding)。
下面为您详细拆解这三个部分的内容与作用:
一、 对象头(Object Header)
对象头是对象内存布局中最复杂、最核心的部分。它通常包含两类信息(如果是数组,则包含三类):
1. Mark Word(标记字段)
- 作用:用于存储对象自身的运行时数据。
- 大小:在 32 位 JVM 中占 4 字节(32 bit),在 64 位 JVM 中占 8 字节(64 bit)。
- 包含内容:
- 对象的 HashCode(哈希码)。
- GC 分代年龄(默认最大为 15,因为占 4 个 bit)。
- 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)。
- 偏向线程 ID、偏向时间戳等。
- 动态结构:为了节省空间,Mark Word 被设计成一个非固定的数据结构。它会根据对象当前的锁状态,复用自己的存储空间。例如,当对象处于“无锁”状态时,它存储的是 HashCode 和年龄;当对象被加上“重量级锁”时,它存储的则是指向互斥量(Monitor)的指针。
2. Klass Pointer(类型指针)
- 作用:指向该对象所属类的元数据(存放在方法区/元空间中)。JVM 通过这个指针来确定该对象是哪个类的实例。
- 大小:
- 32 位 JVM 中占 4 字节。
- 64 位 JVM 中,如果开启了指针压缩(
-XX:+UseCompressedClassPointers,Java 8 默认开启),占 4 字节;如果不开启,占 8 字节。
3. Array Length(数组长度)—— 仅数组对象才有
- 作用:如果对象是一个 Java 数组,对象头中还必须有一块用于记录数组长度的数据。因为 JVM 可以通过普通对象的元数据确定其大小,但无法从数组的元数据中推断出数组的大小。
- 大小:4 字节。
二、 实例数据(Instance Data)
这是对象真正存储的有效信息,也就是我们在程序代码中定义的各种类型的字段(Field)内容。
- 包含内容:不仅包含当前类中定义的字段,还包含从父类继承下来的字段。
- 分配策略(字段重排序):
JVM 并不是严格按照代码中定义的顺序来分配字段内存的。为了节省空间和提高内存访问效率,HotSpot JVM 会进行字段重排序。
默认的分配顺序通常是(从大到小):longs/doubles(8字节)ints/floats(4字节)shorts/chars(2字节)bytes/booleans(1字节)oops(Ordinary Object Pointers,引用类型指针)
注:父类中定义的变量会出现在子类之前。
三、 对齐填充(Alignment Padding)
这部分不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
- 作用:HotSpot JVM 的自动内存管理系统要求对象的起始地址必须是 8 字节的整数倍。换句话说,任何对象的大小都必须是 8 字节的整数倍。
- 触发时机:如果“对象头 + 实例数据”的大小正好是 8 的倍数,就不需要对齐填充;如果不是,就需要补齐缺失的字节。
- 为什么要是 8 的倍数?
主要为了CPU 内存访问的效率。现代 CPU 读取内存不是一个字节一个字节读的,而是按块(Cache Line,通常是 64 字节)读取。强制 8 字节对齐可以避免一个对象跨越多个缓存行,从而提高 CPU 抓取数据的效率,并且底层利用机器码寻址时也更高效。
💡 实例演示(计算一个对象的大小)
假设我们在 64 位 JVM 下(开启指针压缩),有如下类:
java
class MyObject {
int a; // 4字节
byte b; // 1字节
Object c; // 引用类型,开启压缩后占 4字节
}
当我们 new MyObject() 时,它的内存布局如下:
- 对象头(12 字节):
- Mark Word: 8 字节
- Klass Pointer: 4 字节(开启压缩)
- 实例数据(9 字节):
int a: 4 字节Object c: 4 字节(JVM 可能会把引用类型排在前面,或者根据对齐规则调整)byte b: 1 字节- (内部实际可能存在为了让字段对齐的内部填充)
- 对齐填充(3 字节):
- 对象头(12) + 实例数据(9) = 21 字节。
- 21 不是 8 的倍数,需要补齐到 24 字节。因此填充 3 个字节。
最终该对象在内存中占据:24 字节。
🛠️ 进阶工具:如何亲眼查看对象的布局?
在实际开发中,如果不确定对象占多大,可以使用 OpenJDK 提供的利器:JOL (Java Object Layout)。
引入 Maven 依赖:
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
编写代码测试:
java
import org.openjdk.jol.info.ClassLayout;
public class Test {
public static void main(String[] args) {
Object obj = new Object();
// 打印 obj 对象的内存布局
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
运行后,控制台会清晰地打印出每一块内存(OFFSET、SIZE、TYPE DESCRIPTION)的具体分配情况,是深入理解 JVM 内存布局的最佳实践。
右滑查看面试常问