Skip to main content

JEP 534:默认启用紧凑对象头

引言

有些性能优化藏在语言最深处,深到你甚至不知道它的存在。对象头就是这样一个地方——它在每个 Java 对象的最前面默默占着几个字节,你从未声明过它,却每创建一个对象就要为它付一次费。JEP 534 把这块隐形开销砍掉了三分之一到一半,而你一行代码都不用改。

什么是对象头?

Java 堆上的每一个对象,在你亲手定义的那些字段之前,都藏着一段隐藏的元数据,叫做对象头(object header)

  • **每个 Java 对象在字段之前都带着一段隐藏的对象头。**你写 new Point(x, y),实际在内存里躺着的不只是 xy,前面还有一段你看不见的头。
  • 其中的 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 层面的优化,对应用层完全透明。它的美妙之处正在于此:你什么都不用做,收益却出现在真实负载里。这是一份白得的对象瘦身。