JEP450(Compact Object Headers,紧凑对象头)已经成为 JDK 24 的交付目标,并且已合并到了主版本中。
这个目前处于实验阶段的特性通过缩小 HotSpot 中强制对象头的大小来优化堆利用率。这应该会减少整体堆的大小,提高部署密度,并增加数据局部性。
当前的实现情况概述
HotSpot 将所有对象存储在 Java 堆中,Java 堆是进程的“C 堆”的连续区域。在 Java 中始终是通过引用来处理对象,例如:
引用对象的局部变量包含从 Java 方法的堆栈帧到 Java 堆的指针。
引用类型的对象字段从一个 Java 堆位置指向另一个位置。
Java 引用的目标地址始终是对象头的开始处(这在当前版本的 HotSpot 中是强制性的)。
每个对象上都有标头(数组还有一个额外的 32 位标头来存储数组的长度)。标记字是前 64 位,用于特定于实例的元数据,即支持以下特性:
垃圾回收——存储对象的年龄(以及可能的转发指针)
哈希码——存储对象的稳定身份哈希码
锁——存储对象的锁 / 监视器
在某些情况下,标记字将被覆盖并被替换为指向更复杂数据结构的指针。这会使紧凑对象头的实现稍微复杂一些。
在标记字之后是类(或 klass)字,用于计算指向此类类型的每个对象所共享的元数据的指针。这用于方法调用、反射、类型检查等。
klass 元数据(或 klass)保存在元空间中,元空间位于 Java 堆之外,但在 JVM 进程的 C 堆之内。由于它们存在于 Java 堆外,因此 klass 不需要 Java 对象头,而且它们与反射中使用的类对象(真正的 Java 对象)不同。
klass 字最初是标头的一个完整机器字,但这在 64 位的架构上是很浪费的,因此引入了一种称为“压缩类指针”的技术。这将类指针编码为 32 位(通过使用缩放和偏移方法),适用于加载小于 4GB 类文件的任何应用程序。
因此,除了极端情况外,64 位版本的 HotSpot 上的非数组对象要支付 96 位的“标头税”。相比之下,这是轻量级的:直到最近,Python 的标头税还是 308 字节,但 JEP 450 的目的是为了做得更好,将标头的总大小减少到 64 位。
引入紧凑对象头
这个新实现是作为 OpenJDK 的“Project Lilliput”的一部分开发的,它减少了两个目标 64 位平台(x64 和 AArch64)上的对象头大小。
总体目标是:
将目标平台上的吞吐量和延迟开销限制在 5% 内,并且只有在极少数情况下才能达到这一限制
不会在非目标平台上引入可测量到的吞吐量或延迟开销
事实上,目前的测试只显示了极少数的回归(JDK 24 正在对它们进行修复)。到目前为止,亚马逊(Amazon)的测试表明,许多工作负载实际上在吞吐量方面受益,有时甚至会有大幅提升——一些工作负载的 CPU 利用率下降了 30%。
该项目试图利用观察到的事实,即许多 Java 工作负载的平均对象大小较小,只有 32 到 64 字节。这相当于约 20% 的标头税。因此,即使对象头大小略有改进,也可以显著减少堆的占用空间。反过来,这可以提高数据局部性并减少 GC 压力,从而带来进一步的潜在性能优势。
为了实现这种标头的减小,标记字和类字被组合成一个 64 位字,布局如下:
我们应该注意到以下几个方面:
现在有 22 位(而不是 32 位)用于标识对象类类型。这意味着我们可以加载到 JVM 进程中的不同类类型的数量约为 400 万个。
哈希码的大小不会变。
锁定操作不再覆盖标记字。这将保留压缩的类指针。
为了保持对压缩类指针的直接访问,GC 转发操作变得更加复杂。
有 4 个未使用的位保留用于未来的增强(例如 Valhalla 项目)
如果 Java 锁存在争用,那么新的实现需要查找保存锁信息的 辅助数据结 构的地址。这种方法称为“对象监视表”,已经在 JDK 22 中实现了,并由默认启用的新开关 UseObjectMonitorTable
激活。紧凑对象头依赖于此机制。
如果没有发现任何阻碍问题,这一特性将作为 JDK 24 的一部分发布(最初是一个实验特性),发布时间预计在 2025 年 3 月。长期的目标是使该机制成为受支持平台上唯一的标头表示,但这可能需要更多的版本。它还取决于对实际工作负载的广泛测试,目前缺乏性能和其他回归。甚至还有正在进行的探索性工作,以查看是否有可能将标头大小减小到 32 位。
一旦该特性在 JDK 24(测试版或最终版)中可用,应用程序团队可以通过命令行开关 -XX:UseCompactObjectHeaders
来激活该新特性以测试他们的工作负载,并寻找与之相关的性能差异,从而为长期目标提供帮助。
原文链接:
评论