JEP 534:默认启用紧凑对象头
引言
有些性能优化藏在语言最深处,深到你甚至不知道它的存在。对象头就是这样一个地方——它在每个 Java 对象的最前面默默占着几个字节,你从未声明过它,却每创建一个对象就要为它付一次费。JEP 534 把这块隐形开销砍掉了三分之一到一半,而你一行代码都不用改。
什么是对象头?
Java 堆上的每一个对象,在你亲手定义的那些字段之前,都藏着一段隐藏的元数据,叫做对象头(object header)。
- **每个 Java 对象在字段之前都带着一段隐藏的对象头。**你写
new Point(x, y),实际在内存里躺着的不只是x和y,前面还有一段你看不见的头。 - 其中的 mark word 保存着对象的哈希码、GC 年龄、锁状态等运行时信息。
- 此外还有一个类指针(class pointer),指向该对象的类型信息,让 JVM 知道"这是个什么对象"。
- 在 64 位 HotSpot 上,这部分历来要占 96 位,也就是 12 个字节。
对象头大小为什么重要
你可能觉得 12 字节微不足道。但关键在于——典型的 Java 堆里,绝大多数都是小对象。
- 一个只有两三个字段的对象,**它的对象头可能和它自己的字段一样大,甚至更大。**12 到 16 字节的头,装在一个同样只有十几字节有效数据的对象上,开销占比轻松超过一半。
- 对象越小,头部占比越高。而这直接侵蚀了缓存利用率:CPU 缓存行是按固定大小(通常 64 字节)搬运数据的,如果缓存行里塞满了对象头元数据,能装下的有效数据就更少了。你每次访存,都在为一堆你不关心的头信息买单。
- **堆上流量更大,意味着更多的 GC 工作和更高的内存带宽消耗。**对象头占掉的每一个字节,都要被分配、被复制、被回收、被扫描。海量小对象乘上这份固定开销,累积起来就是可观的性能损失。
JEP 534 改变了什么
JEP 534 把 Project Lilliput 的**紧凑对象头(compact object headers)**设为默认。
- 紧凑对象头成为默认行为。
- 在 64 位平台上,对象头缩小到 64 位,也就是 8 个字节——相比原来的 12~16 字节,直接砍掉了三分之一到一半。
- 实现这一点的核心技巧是:**把 mark word 和压缩类指针合并进同一个 64 位的字里。**原本各占一块的两份信息,被巧妙地打包进了一个字。
- 演进路线上,这个特性在 JDK 24 作为实验特性发布,在 JDK 25 成为正式产品选项,到 JDK 27 终于水到渠成地成为默认行为。
影响:实测效果与兼容性提示
实测效果:
- JEP 报告称,在一次 SPECjbb2015 实验中 GC 次数减少了 15%,在一个高度并行的 JSON 解析器基准中 耗时减少了 10%。
- **缓存局部性更好,GC 次数更少。**同样的缓存行能装下更多有效对象,GC 需要处理的堆流量也随之下降。
- **分配压力下降带来吞吐提升。**更小的对象意味着分配更快、回收更省,整体吞吐随之改善。
兼容性提示:
- 如果遇到问题,可以临时用
-XX:-UseCompactObjectHeaders回退到旧的对象头布局。 - **假设了对象头布局的 agent 或工具可能需要更新。**极少数直接依赖对象头内存布局的底层工具(比如某些性能分析器、序列化框架或 JVMTI agent)需要适配新布局。
- 类指针的空间是有上限的,因此在类数量极其庞大的场景下,这是一个需要留意的边界。
不过对于绝大多数应用来说,这些都不是问题——不用改一行代码就能受益。
结论
不用改动任何代码,只要升级到 JDK 27,你的对象就自动"瘦身"了,性能收益随之而来。这是一次纯粹发生在 JVM 层面的优化,对应用层完全透明。它的美妙之处正在于此:你什么都不用做,收益却出现在真实负载里。这是一份白得的对象瘦身。