Skip to content

对象内存布局与访问定位

对象在堆里长什么样

对象在堆中不是「随意摆放」的。JVM 对对象的内存布局有严格的规定,理解它有助于理解对象头、锁优化、以及如何排查内存问题。

对象的内存布局

HotSpot 中,对象在堆中的内存布局分为三部分:

┌──────────────────────────────────────────────────┐
│                  对象内存布局                       │
│                                                   │
│  ┌─────────────────────────────────────────────┐ │
│  │              对象头(Header)                   │ │
│  │                                             │ │
│  │   ┌────────────────┐  ┌─────────────────┐  │ │
│  │   │   Mark Word     │  │  类型指针(klass) │  │ │
│  │   │   (8 bytes)    │  │   (4/8 bytes)   │  │ │
│  │   │  哈希码/GC分代/锁  │  │  指向类元数据   │  │ │
│  │   └────────────────┘  └─────────────────┘  │ │
│  └─────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────┐ │
│  │            实例数据(Instance Data)            │ │
│  │                                             │ │
│  │   ┌──────────────────┐ ┌────────────────┐  │ │
│  │   │   name (引用)    │ │  age (int)     │  │ │
│  │   │   4/8 bytes     │ │   4 bytes      │  │ │
│  │   └──────────────────┘ └────────────────┘  │ │
│  └─────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────┐ │
│  │            对齐填充(Padding)                  │ │
│  │   保证对象大小是 8 的倍数                      │ │
│  └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

对象头(Object Header)

对象头是对象在内存布局中最核心的部分。HotSpot 的对象头分两部分:

1. Mark Word(8 字节 / 64位系统)

Mark Word 存储了对象的运行时数据,高度压缩:

状态Mark Word 内容(64 位)
无锁状态对象哈希码(25位) + GC分代年龄(4位) + 偏向锁标志(1位) + 锁标志位(4位)
轻量级锁指向栈中锁记录的指针(62位) + 锁标志位
重量级锁指向互斥量(重量级锁)的指针(62位) + 锁标志位
GC 标记空(62位) + 锁标志位
偏向锁线程 ID(54位) + Epoch(2位) + 分代年龄(4位) + 偏向锁标志(1位) + 锁标志位

Mark Word 的大小是固定的 8 字节,但内容会根据对象状态动态变化——这是 JVM 空间换时间的经典设计。

2. 类型指针(Klass Pointer)

指向方法区中类元数据的指针。

系统类型指针大小说明
32 位系统4 字节
64 位系统(默认)8 字节
64 位系统 + 压缩指针4 字节-XX:+UseCompressedClassPointers

对象大小计算

java
public class ObjectLayout {
    static class A {
        int a;           // 4 字节
    }

    static class B {
        int a;           // 4 字节
        int b;           // 4 字节
    }

    static class C {
        int a;           // 4 字节
        long b;           // 8 字节(8 字节对齐)
        int c;           // 4 字节
    }

    static class D {
        Object o;         // 引用:4 字节(开启压缩指针)
    }

    static class E {
        int i;            // 4 字节
        Object o;         // 引用:4 字节
        double d;         // 8 字节
    }
}

用 JOL 查看实际布局

java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

// 需要添加依赖:org.openjdk.jol:jol-core

public class JOLDemo {
    public static void main(String[] args) {
        System.out.println(VM.current().details());

        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

输出示例:

java.lang.Object object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000005 (biasable; lasts: 0)
  8   4                    (object header: class)    0x0000000000000001
 12   4                    (object alignment/padding) (loss due to the next object alignment)
Instance size: 16 bytes

对象访问定位:如何找到堆中的对象

Java 程序通过引用访问对象。引用存储在虚拟机栈的局部变量表中。但引用本身只是一个地址,JVM 如何通过引用找到堆中的对象?

有两种主流方式:

方式一:句柄访问(早期使用)

┌─────────────────────────────────────────────┐
│  虚拟机栈  │  局部变量表                       │
│  ┌────────┐  ┌─────────┐                   │
│  │ 引用   │→│ 句柄地址 │                   │
│  └────────┘  └────┬────┘                   │
└────────────────────┼─────────────────────────┘


              ┌──────────────────┐
              │    句柄池         │
              │                   │
              │  ┌──────────────┐ │
              │  │ 对象实例地址  │ │
              │  └──────────────┘ │
              │  ┌──────────────┐ │
              │  │ 类型数据指针  │ │
              │  └──────────────┘ │
              └──────────────────┘
                      │       │
                      ▼       ▼
               ┌──────────┐  ┌──────────┐
               │  对象实例  │  │ 类元数据  │
               │  (堆)   │  │(方法区) │
               └──────────┘  └──────────┘

优点:对象被 GC 移动时(复制算法整理内存),只需修改句柄池中的指针,引用本身不变。

方式二:直接指针访问(HotSpot 使用)

┌─────────────────────────────────────────────┐
│  虚拟机栈  │  局部变量表                       │
│  ┌────────┐                                │
│  │ 引用   │───────────────────────────┐      │
│  └────────┘                            │      │
└────────────────────────────────────────┼──────┘


                 ┌──────────────────────────────────┐
                 │        对象头         │  实例数据  │
                 │  Mark Word │ 类型指针 │            │
                 └────────────┴──────────┴───────────┘
                              │              │
                              ▼              ▼
                        ┌──────────┐  ┌──────────┐
                        │ 类元数据  │  │ 对象字段  │
                        │(方法区) │  │  (堆)  │
                        └──────────┘  └──────────┘

优点:访问速度快(少一次指针跳转)。HotSpot 采用这种方式。

对齐填充的影响

HotSpot 的对象起始地址必须是 8 字节的倍数。不足的部分用空字节填充。

java
public class PaddingDemo {
    static class Small {
        int a;  // 4 字节
        // 需要 4 字节 padding 才能对齐
    }

    static class Optimal {
        int a;  // 4 字节
        int b;  // 4 字节
        // 刚好 8 字节,无 padding
    }

    static class Worst {
        int a;  // 4 字节
        int b;  // 4 字节
        int c;  // 4 字节
        int d;  // 4 字节
        int e;  // 4 字节
        // 20 字节 → padding 4 字节 → 24 字节
    }
}

padding 的实战影响

字段的声明顺序会影响对象大小:

java
public class FieldOrder {
    // 方式一:浪费空间
    static class BadOrder {
        long a;   // 8 字节
        int b;    // 4 字节
        int c;    // 4 字节
        // padding 4 字节
        // 总计:24 字节
    }

    // 方式二:节省空间
    static class GoodOrder {
        int b;    // 4 字节
        int c;    // 4 字节
        long a;   // 8 字节
        // 无 padding
        // 总计:16 字节
    }
}

所以,把大字段放在前面,小字段放在后面,可以减少 padding。

本节小结

对象的内存布局:

部分大小说明
Mark Word8 字节(64位)存储哈希码、GC 分代年龄、锁信息
类型指针4/8 字节指向方法区的类元数据
实例数据按字段类型对象字段的实际内容
对齐填充0~7 字节保证对象大小是 8 的倍数

HotSpot 的对象访问通过直接指针,引用直接指向对象,跳过句柄池,访问速度更快。

理解对象布局,有助于理解偏向锁、轻量级锁的底层实现,以及为什么字段顺序会影响对象大小。

到这里,「JVM 运行时数据区」全部章节都已完成。

基于 VitePress 构建