引言:对象在内存中的真实面貌

当你创建一个对象时:

User user = new User("张三", 25);

这个对象在JVM堆中究竟占用多少字节?内存是如何布局的?

答案可能出乎意料:即使 User 类只有两个字段(name和age),这个对象也可能占用 32字节甚至更多

为什么?

  • 对象不仅包含实例数据(字段值)
  • 还包含 对象头(存储对象元信息)
  • 还可能有 对齐填充(保证内存对齐)

理解对象的内存布局是掌握JVM内存优化、锁机制、GC原理的基础。


对象的内存布局结构

三大组成部分

在HotSpot虚拟机中,对象在内存中分为3个部分:

┌─────────────────────────────────────────────────┐
│              对象内存布局                        │
├─────────────────────────────────────────────────┤
│  1. 对象头 (Object Header)                      │
│     ├─ Mark Word(8字节,64位JVM)               │
│     └─ 类型指针(4字节,开启指针压缩)            │
│                                                 │
│  2. 实例数据 (Instance Data)                    │
│     · 字段1: String name                        │
│     · 字段2: int age                            │
│                                                 │
│  3. 对齐填充 (Padding)                          │
│     · 确保对象大小是8字节的倍数                  │
└─────────────────────────────────────────────────┘

对象头(Object Header)

对象头的组成

对象头包含两部分:

1️⃣ Mark Word(标记字)

大小

  • 32位JVM:4字节
  • 64位JVM:8字节

作用:存储对象的 运行时元数据

存储内容(64位JVM为例):

锁状态29位2位1位4位1位锁标志位2位锁标志位
无锁unused对象hashCode(31位)unused分代年龄(4位)001
偏向锁线程ID(54位)epoch(2位)unused分代年龄(4位)101
轻量级锁指向栈中锁记录的指针(62位)00
重量级锁指向互斥量的指针(62位)10
GC标记11

关键信息

  • 哈希码(HashCode):调用 obj.hashCode() 时生成并存储
  • 分代年龄(Age):对象在Survivor区经历的Minor GC次数(最大15)
  • 锁状态标志:无锁、偏向锁、轻量级锁、重量级锁
  • 线程ID:偏向锁时,记录持有锁的线程ID

2️⃣ 类型指针(Class Pointer)

大小

  • 64位JVM(未开启指针压缩):8字节
  • 64位JVM(开启指针压缩):4字节

作用:指向对象的类元数据(Class对象)。

为什么需要类型指针?

  • 确定对象的类型
  • 调用方法时查找方法表
  • 反射操作

对象头的总大小

64位JVM(开启指针压缩,默认)

  • Mark Word:8字节
  • 类型指针:4字节
  • 总计:12字节

64位JVM(未开启指针压缩)

  • Mark Word:8字节
  • 类型指针:8字节
  • 总计:16字节

实例数据(Instance Data)

定义

实例数据 存储对象的 字段值,包括:

  • 继承自父类的字段
  • 当前类定义的字段

字段存储顺序

HotSpot的存储规则

  1. 相同宽度的字段分配在一起
  2. 父类字段在子类字段之前
  3. 宽度从大到小排列:long/double(8字节) → int/float(4字节) → short/char(2字节) → byte/boolean(1字节) → 引用类型(4或8字节)

各类型字段的大小

Java类型大小(字节)说明
byte18位
boolean18位(实际只用1位,但占1字节)
short216位
char216位
int432位
float432位
long864位
double864位
引用类型4或8开启指针压缩为4字节,否则8字节

示例:实例数据布局

class User {
    private String name;  // 引用类型,4字节(开启指针压缩)
    private int age;      // int,4字节
    private boolean active;  // boolean,1字节
}

实例数据布局

┌────────────────────────┐
│  name  (String引用)     │  4字节
├────────────────────────┤
│  age   (int)           │  4字节
├────────────────────────┤
│  active (boolean)      │  1字节
├────────────────────────┤
│  对齐填充               │  3字节(补齐到8的倍数)
└────────────────────────┘

实例数据总大小: 12字节

对齐填充(Padding)

为什么需要对齐填充?

HotSpot要求:对象大小必须是 8字节的倍数

原因

  • CPU访问内存时,按地址对齐的数据更高效
  • 减少CPU缓存miss
  • 统一内存管理

规则

  • 如果对象头 + 实例数据不是8的倍数,需要 补齐
  • 补齐的字节称为 对齐填充(Padding)

示例:对齐填充计算

class SimpleObject {
    private int value;  // 4字节
}

内存布局

┌────────────────────────┐
│  对象头 (Mark Word)     │  8字节
├────────────────────────┤
│  对象头 (类型指针)      │  4字节
├────────────────────────┤
│  value (int)           │  4字节
├────────────────────────┤
│  对齐填充               │  0字节(12+4=16,已是8的倍数)
└────────────────────────┘

对象总大小: 16字节

class TinyObject {
    private byte value;  // 1字节
}

内存布局

┌────────────────────────┐
│  对象头 (Mark Word)     │  8字节
├────────────────────────┤
│  对象头 (类型指针)      │  4字节
├────────────────────────┤
│  value (byte)          │  1字节
├────────────────────────┤
│  对齐填充               │  3字节(12+1=13,补齐到16)
└────────────────────────┘

对象总大小: 16字节

指针压缩(Compressed Oops)

什么是指针压缩?

Oops(Ordinary Object Pointers):普通对象指针。

指针压缩:将64位指针压缩为32位,节省内存。

开启条件

  • 64位JVM
  • 堆大小 < 32GB
  • JDK 6 Update 23及之后默认开启

压缩原理

核心思想:利用对象地址的对齐特性。

  • 对象地址都是 8字节对齐 的(末尾3位始终为0)
  • 可以用32位存储 地址右移3位 后的值
  • 寻址时,将32位值 左移3位 恢复为64位地址

可寻址范围

  • 32位指针:2^32 × 8 = 32GB

示例

原始64位地址: 0x0000 0000 1234 5678  (假设末尾3位为000)
压缩后32位:   0x1234 5678 (右移3位,去掉末尾000)

恢复64位地址: 0x1234 5678 << 3 = 0x0000 0000 1234 5678

指针压缩的参数

# 查看是否开启指针压缩(默认开启)
java -XX:+PrintFlagsFinal -version | grep UseCompressedOops
# 输出: bool UseCompressedOops = true

# 关闭指针压缩(不推荐)
java -XX:-UseCompressedOops MyApp

指针压缩的影响

开启指针压缩(默认)

  • 类型指针:4字节
  • 引用类型字段:4字节
  • 节省内存约 20-30%

关闭指针压缩

  • 类型指针:8字节
  • 引用类型字段:8字节
  • 内存占用增加

使用JOL工具查看对象布局

什么是JOL?

JOL(Java Object Layout) 是OpenJDK提供的官方工具,用于查看对象的精确内存布局。

添加JOL依赖

<!-- Maven -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

示例1:简单对象

import org.openjdk.jol.info.ClassLayout;

public class JOLDemo {
    static class User {
        private String name;
        private int age;
    }

    public static void main(String[] args) {
        User user = new User();
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

输出

com.example.JOLDemo$User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000000000000001
  8   4                    (object header: class)    0x00001234
 12   4            java.lang.String name                null
 16   4                             int age                 0
 20   4                                 (object alignment gap)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

解读

  • OFF:字段在对象中的偏移量(字节)
  • SZ:字段大小(字节)
  • 0-7:Mark Word(8字节)
  • 8-11:类型指针(4字节)
  • 12-15:name字段(4字节引用)
  • 16-19:age字段(4字节int)
  • 20-23:对齐填充(4字节)
  • 总大小:24字节

示例2:空对象

static class EmptyObject {
    // 没有字段
}

public static void main(String[] args) {
    EmptyObject obj = new EmptyObject();
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

输出

com.example.JOLDemo$EmptyObject object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001
  8   4        (object header: class)    0x00005678
 12   4        (object alignment gap)
Instance size: 16 bytes

解读

  • 空对象也占用 16字节(对象头12字节 + 对齐填充4字节)

示例3:数组对象

public static void main(String[] args) {
    int[] array = new int[5];
    System.out.println(ClassLayout.parseInstance(array).toPrintable());
}

输出

[I object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001
  8   4        (object header: class)    0x00001234
 12   4        (array length)            5
 16  20   int [I.<elements>             N/A
 36   4        (object alignment gap)
Instance size: 40 bytes

解读

  • 数组对象比普通对象多4字节(存储数组长度)
  • 对象头:12字节(Mark Word 8 + 类型指针 4)
  • 数组长度:4字节
  • 数组元素:5个int = 20字节
  • 对齐填充:4字节
  • 总大小:40字节

实战案例:对象大小优化

案例:字段顺序对对象大小的影响

优化前

class User {
    private boolean active;  // 1字节
    private int age;         // 4字节
    private String name;     // 4字节
}

内存布局

对象头: 12字节
boolean active: 1字节
对齐填充: 3字节(补齐到4的倍数)
int age: 4字节
String name: 4字节
总大小: 24字节

优化后

class User {
    private String name;     // 4字节
    private int age;         // 4字节
    private boolean active;  // 1字节
}

内存布局

对象头: 12字节
String name: 4字节
int age: 4字节
boolean active: 1字节
对齐填充: 3字节
总大小: 24字节

结论:本例中两种顺序大小相同,因为最终都需要对齐填充。但在复杂对象中,合理排列字段可以减少对齐填充的浪费。


常见问题与误区

❌ 误区1:对象只包含字段数据

真相

  • 对象包含对象头(12或16字节)
  • 即使空对象也占用16字节
  • 实例数据只是对象的一部分

❌ 误区2:boolean只占1位

真相

  • boolean 字段 占用1字节
  • boolean 数组元素 占用1字节
  • boolean在栈上可能优化为1位(具体实现相关)

❌ 误区3:对象大小 = 字段大小之和

真相

  • 对象大小 = 对象头 + 实例数据 + 对齐填充
  • 必须考虑对象头(12或16字节)
  • 必须考虑对齐填充(8字节的倍数)

总结

核心要点

  1. 对象内存布局包含3部分:对象头、实例数据、对齐填充

  2. 对象头包含Mark Word和类型指针,存储对象元数据和类型信息

  3. Mark Word存储运行时信息:哈希码、分代年龄、锁状态

  4. 对齐填充确保对象大小是8字节的倍数

  5. 指针压缩节省内存20-30%,默认开启

  6. 使用JOL工具可以精确查看对象布局

阶段总结

至此,我们完成了 第三阶段:内存结构篇 的全部8篇文章:

✅ 11. JVM内存结构全景:5大区域详解 ✅ 12. 程序计数器:最小的内存区域 ✅ 13. 虚拟机栈:方法执行的内存模型 ✅ 14. 本地方法栈:Native方法的秘密 ✅ 15. 堆内存:对象的诞生地与分代设计 ✅ 16. 方法区:类元数据的存储演变 ✅ 17. 直接内存:堆外内存与NIO ✅ 18. 对象的内存布局:对象头、实例数据、对齐填充

学习成果

  • 全面理解JVM内存结构
  • 掌握各内存区域的作用和特点
  • 理解对象在内存中的存储方式
  • 为后续学习垃圾回收打下坚实基础

与下篇文章的衔接

下一篇文章,我们将进入 第四阶段:垃圾回收篇,学习 什么是垃圾?如何判断对象已死?,理解对象存活判定的两种算法。


参考资料


下一篇预告:《什么是垃圾?如何判断对象已死?》 深入理解垃圾的定义、对象存活判定的两种算法(引用计数 vs 可达性分析)、以及finalize方法的秘密。