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