一个空Object对象在堆中占16字节:开启压缩指针时为8字节Mark Word+4字节Klass Pointer+4字节对齐填充;关闭时为8+8=16字节,无需填充。

Object 对象在堆中占多少字节?
默认开启压缩指针(-XX:+UseCompressedOops)的 64 位 JVM 下,new Object() 占 12 字节对象头 + 4 字节对齐填充 = 16 字节。不开启压缩指针时是 16 字节对象头 + 0 填充 = 16 字节(刚好对齐),但实际更耗内存。
关键点在于:空 Object 没有实例数据,所以只看对象头和对齐填充。对象头由两部分组成:Mark Word(运行时元数据)和 Klass Pointer(类型指针)。64 位未压缩时各占 8 字节;开启压缩后 Klass Pointer 压成 4 字节,Mark Word 仍为 8 字节,共 12 字节 —— 必须补 4 字节凑成 8 的倍数,所以最终是 16 字节。
常见误判:
- 认为“空对象就是 0 字节”或“只占对象头 8 字节”——忽略对齐要求
- 用
Runtime.totalMemory() - Runtime.freeMemory()估算对象大小——受 GC、分配策略干扰极大,不可靠 - 在非 HotSpot 虚拟机(如 Zing、GraalVM)上套用 HotSpot 的布局规则——结构可能不同
实例数据字段怎么排列?
实例数据不是按源码顺序一字排开的,而是按字段宽度分组、父类优先、窄字段可插入空隙(当 -XX:+CompactFields 开启,默认 true)。
例如这个类:
class Customer {
int id; // 4 字节
boolean flag; // 1 字节
long ts; // 8 字节
}
在开启压缩指针的 64 位 JVM 中,字段实际布局可能是:id(4)+ flag(1)+ 3 字节填充 + ts(8)——因为 long 需要 8 字节对齐,flag 后必须补到下一个 8 字节边界起点。
影响因素包括:
-
boolean、byte这类 1 字节字段,常被挤进其他字段的空隙里,不一定会单独占一行 - 父类字段一定排在子类字段前面,哪怕子类字段声明更早
-
-XX:-CompactFields会禁用窄字段插入,导致更多填充字节,对象体积变大
为什么 GC 年龄最大是 15?
因为 Mark Word 中 GC 分代年龄用的是 4 位二进制存储,最大值是 1111(二进制)= 15(十进制)。第 5 次 Survivor 复制后,年龄字段就溢出了,JVM 直接将其晋升到老年代。
这个限制和对象布局强相关:它不是 Java 语言规范定的,而是 HotSpot 在有限空间(4 位)里做的权衡。你无法通过 JVM 参数扩大它;改源码重编译也不现实。
注意:
- 这个 4 位是紧挨着锁标志位、哈希码等复用同一块
Mark Word空间的,不是独立字段 - 对象刚创建时年龄为 0,第一次 GC 后幸存才变成 1
- 如果对象在 Eden 区直接被大对象分配策略绕过 Survivor,就不会走年龄计数逻辑
怎么验证真实内存布局?
别猜,用 JOL(Java Object Layout)工具直出二进制视图。它能精确显示每个字段偏移量、占用字节、填充位置。
添加依赖:
<dependency><groupid>org.openjdk.jol</groupid><artifactid>jol-core</artifactid><version>0.17</version></dependency>
代码示例:
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
输出里重点关注三列:OFFSET(起始偏移)、SIZE(字节数)、TYPE DESCRIPTION(类型说明)。你会看到 mark word 在 offset 0,klass pointer 在 offset 8(压缩后在 offset 8,但只占 4 字节),最后几行是 padding。
容易被忽略的细节:
-
JOL默认使用当前 JVM 启动参数(含压缩指针开关),但如果你在 IDE 里直接 run,可能没继承 maven profile 或脚本里的 JVM 参数 - 输出中的 “(something)” 注释(比如
(object header))是 JOL 自己加的,不是 JVM 标准术语 - 数组对象会多出一个
array length字段,在对象头末尾,占 4 字节(压缩后)或 8 字节(未压缩)










