基于本文回答
0
评论

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 会进行字段重排序
    默认的分配顺序通常是(从大到小):
    1. longs/doubles (8字节)
    2. ints/floats (4字节)
    3. shorts/chars (2字节)
    4. bytes/booleans (1字节)
    5. 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() 时,它的内存布局如下:

  1. 对象头(12 字节)
    • Mark Word: 8 字节
    • Klass Pointer: 4 字节(开启压缩)
  2. 实例数据(9 字节)
    • int a: 4 字节
    • Object c: 4 字节(JVM 可能会把引用类型排在前面,或者根据对齐规则调整)
    • byte b: 1 字节
    • (内部实际可能存在为了让字段对齐的内部填充)
  3. 对齐填充(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 内存布局的最佳实践。

右滑查看面试常问