Java对象在堆中按对象头、实例数据、对齐填充三部分连续布局;对象头含Mark Word和Klass Pointer,实例数据按字段宽度降序排列并继承优先,对齐填充确保8字节整数倍。

Java中没有直接暴露内存地址的操作,所以无法像C/C++那样通过指针或地址值去“看到”对象在堆中的真实位置。但理解对象在堆内存中的逻辑存储结构,对掌握GC、对象布局、锁升级、序列化等机制非常有帮助——关键不是记住地址数字,而是把握对象头、实例数据、对齐填充这三部分的组织逻辑和作用。
对象在堆中不是“裸数据”,而是有固定结构的内存块
每个Java对象(除数组外)在堆中占用连续的一段内存,其内部按JVM规范划分为三个区域:
- 对象头(Header):包含Mark Word(存储哈希码、GC分代年龄、锁状态标志、线程持有的锁指针等)和Klass Pointer(指向类元数据的指针,用于确定对象属于哪个类);在64位JVM开启指针压缩(默认开启)时,Klass Pointer占4字节
- 实例数据(Instance Data):真正存放成员变量的地方,按字段宽度从大到小排序(long/double → int/float → short/char → byte/boolean),父类字段排在子类字段前面
- 对齐填充(Padding):保证整个对象大小是8字节的整数倍(即内存对齐),便于CPU高效读取;这部分无实际语义,纯属填充字节
内存地址本身不可见,但可通过工具间接观察布局
虽然Java语言层面禁止获取对象地址(如没有 & 运算符),但借助JOL(Java Object Layout)工具可以精确计算并打印对象的内存布局:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 添加依赖:
org.openjdk.jol:jol-core - 调用
ClassLayout.parseInstance(obj).toPrintable()即可输出字段偏移、大小、对齐信息 - 例如一个空的
Object实例,在64位+指针压缩下通常占12字节(8字节Mark Word + 4字节Klass Pointer),再加4字节对齐填充 → 共16字节
引用变量本质是“句柄或直接指针”,不等于地址值
声明 MyClass obj = new MyClass(); 时,obj 是栈上的局部变量,它存储的不是内存地址数字,而是JVM内部用于定位堆中对象的引用值。这个值的具体形式取决于JVM实现:
立即学习“Java免费学习笔记(深入)”;
- HotSpot采用直接指针方式:栈中引用直接保存对象在堆中的起始地址(即指向对象头);访问快,但GC移动对象时需更新所有引用
- (历史上也有句柄池方式:引用指向句柄池中的条目,句柄再存对象地址和类型指针;现代HotSpot已不用)
- 这个“地址”是JVM管理的逻辑地址,用户代码既不能打印它,也不能参与运算 —— 它只是JVM内部调度的桥梁
理解结构比纠结地址更有实际价值
知道对象怎么放,能帮你解释很多现象:
- 为什么
new Object()占16字节,而new Long(1)占24字节?(对象头12 + long字段8 - 对齐后共24) - 为什么把
long字段放在前面能略微提升缓存命中率?(减少CPU预取时跨缓存行) - 为什么
synchronized锁对象时,会修改Mark Word里的锁标志位?(因为锁信息就存在对象头里) - 为什么使用
Unsafe.objectFieldOffset()能拿到字段相对于对象起始的偏移量?(正是基于上述布局规则计算得出)

















