【JVM】对象内存布局

Java 对象在 HotSpot JVM 中的内存布局,可以从宏观结构 、对象头的动态机制 、实例数据的排列规则 ,以及如何量化验证几个层面来理解。


整体结构:三块区域(数组对象多一块)

在 HotSpot JVM 中,普通对象在堆内存中的布局固定分为三部分;数组对象会额外增加一个字段:

组成部分 核心作用
对象头 存储运行时元数据(Mark Word)与类型指针(Klass Pointer),是锁、GC、哈希等机制的控制中枢
实例数据 真正存储的成员变量值,包括本类与父类继承的字段
对齐填充 不是业务数据,仅用于保证对象总大小为 8 字节的整数倍

对象总大小的强制约束是:对象头 + 实例数据 + 对齐填充 必须是 8 的倍数。


对象头:Mark Word 与 Klass Pointer

在 64 位 JVM 默认开启指针压缩时,普通对象的对象头为 12 字节 (8 字节 Mark Word + 4 字节 Klass Pointer);数组对象在此基础上再增加 4 字节数组长度,对象头达到 16 字节。

Mark Word:动态复用的 8 字节

Mark Word 不是固定格式,而是根据对象当前锁状态 复用同一块 64 位空间。低 3 位是识别状态的核心:

  • 无锁 :001,存储 identity hash code、GC 分代年龄等;
  • 偏向锁 :101,存储持有偏向锁的线程 ID、偏向时间戳;
  • 轻量级锁 :00,存储指向线程栈中 Lock Record 的指针;
  • 重量级锁 :10,存储指向 Monitor 对象的指针;
  • GC 标记 :11。

有一个容易踩坑的细节:调用 System.identityHashCode() 会直接导致该对象的偏向锁被撤销,且后续不会再重新偏向。原因是偏向锁状态下 Mark Word 的空间被线程 ID 占用,没有位置再容纳哈希码;一旦写入哈希码,JVM 就必须退回到无锁或轻量级锁路径。

Klass Pointer:指向"这个对象是什么类"

Klass Pointer 指向方法区(元空间)中该对象对应的 Class 元数据,JVM 通过它完成方法调用和类型校验。它的默认大小取决于指针压缩:

  • 开启 (-XX:+UseCompressedClassPointers,默认):4 字节;
  • 关闭 :8 字节。

指针压缩:为什么默认开启,又为什么和 32GB 有关

64 位 JVM 如果每个引用都占 8 字节,内存开销会显著增大。HotSpot 从 JDK 6u14 开始在 64 位系统中默认开启 -XX:+UseCompressedOops,将对象引用(oop)也压缩为 4 字节。

压缩之所以可行,依赖一个关键前提:对象 8 字节对齐 。对齐后,所有对象的起始地址二进制末尾 3 位始终为 0。JVM 在存储引用时丢弃这 3 位,用 32 位表示"第几个 8 字节块",从而将寻址范围扩展到 2³² × 8 = 32GB。

这也直接解释了经典的经验规则:堆内存一旦超过 32GB,压缩指针机制失效,4 字节引用退回 8 字节,实际可用内存和指针密度都会出现"反直觉"的变化。因此,堆大小设置为略低于 32GB(如 30~31GB)通常是更稳妥的选择。


实例数据:字段不按声明顺序存

一个常见的误解是字段按代码书写顺序排列。实际上,HotSpot 会对实例字段进行重排序,目标是最小化字段之间的内存间隙。

默认的分配优先级从大到小为:long/double → int/float → short/char → byte/boolean → 引用类型 。同时,父类的字段永远排在子类字段之前,父类字段结束位置对齐到 4 字节边界后,再放置子类字段。

例如:

java 复制代码
class Demo {
    byte a;
    int b;
    short c;
}

如果按声明顺序存,byte 之后为了放 int 需要插入 3 字节间隙,占用会膨胀;重排序为 int b → short c → byte a 后,数据部分紧凑排列,再配合对齐填充,总大小可以从 24 字节降到 16 字节。


对齐填充与最小对象大小

对齐填充的唯一硬性目标是让总大小成为 8 的倍数。它看起来是"浪费",但承担着几项底层职能:让 CPU 以 8 字节为单位高效访问;维持指针压缩的地址对齐前提;让 GC 以固定步长快速枚举对象。

一个空的 new Object() 在 64 位、开启压缩指针的 HotSpot 中:

  • Mark Word:8 字节
  • Klass Pointer:4 字节
  • 对齐填充:4 字节(补到 8 的倍数)

总大小 16 字节 。所以 Java 对象的最小大小就是 16 字节,不存在更小的对象。


量化验证:用 JOL 直接看布局

对象内存布局不适合靠猜,OpenJDK 官方的 JOL(Java Object Layout) 工具可以直接打印每个字段的偏移量、大小和对象头内容。

引入依赖后:

xml 复制代码
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

打印一个空对象:

java 复制代码
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

输出中 OFFSET 是字段在对象内的起始字节偏移,SIZE 是占用字节数,DESCRIPTION 中 (object header) 对应对象头,(alignment/padding gap) 对应对齐填充。通过它可以直接确认 Mark Word 的低位锁标志、Klass Pointer 是否被压缩,以及字段重排序后的实际偏移。

其他对象例子:


理解对象内存布局的实际价值,在于它把多个看似独立的知识点串在一起:Mark Word 解释了 synchronized 锁升级的物理基础;指针压缩与 8 字节对齐共同决定了 32GB 堆界限;字段重排序影响对象占用密度;对齐填充和缓存行填充则直接关系到伪共享问题的解决方式 (JDK 8+ 的 @Contended 本质上也是在利用布局规则让高频字段独占缓存行)。

相关推荐
何中应1 小时前
【Go-1】变量&常量定义
开发语言·后端·golang
CoderYanger1 小时前
Java EE 进阶:6.SpringBoot 配置文件(第二弹)
java·spring boot·spring·面试·职场和发展·java-ee·程序员创富
文丑颜不良啊1 小时前
LeetCode1681~1700题目解析
java·leetcode
愚农搬码1 小时前
电子表单、动态表单、表单设计器和表单引擎到底有什么区别?
java·后端
CarIise1 小时前
Spring 框架AOP与 XML 基础
java·开发语言
m0_639906941 小时前
SpringBoot 实现大模型统一调度网关|对接 OpenAI、Claude 协议差异与适配方案
java·spring boot·spring
雪芽蓝域zzs1 小时前
第 1 章:IDEA 全局配置 Maven + 新建 SpringBoot3 Maven 项目
java·maven·intellij-idea
geovindu1 小时前
rust:N-Barrier Pattern
开发语言·后端·设计模式·rust·屏障模式·并发型模式
AI宇宙狂想家2 小时前
开源 AI 视频工具进化史:从难用到好用,OpenMontage 改了什么
开发语言·前端·人工智能·开源·ai创业·openmontage