Skip to content

JEP 250: Store Interned Strings in CDS Archives | 将字符串存储在 CDS 存档中

摘要

在 类数据共享(CDS)存档 中存储已编码的字符串。

目标

  • 通过在不同的 JVM 进程之间共享 String 对象和底层的 char 数组对象来减少内存消耗。
  • 仅支持 G1 GC 的共享字符串。共享字符串需要固定的区域,而 G1 是唯一支持固定区域的 HotSpot GC。
  • 仅支持压缩对象和类指针的 64 位平台。
  • 在启动时间、字符串查找时间、GC 暂停时间或运行时性能方面,使用通常的基准测试不会出现明显的下降(< 2-3%)。

非目标

  • 减少启动时间不是目标。
  • 不支持除 G1 以外的其他类型的 GC。
  • 不支持 32 位平台。

动机

当前,当 CDS 将类存储到存档中时,常量池中的 CONSTANT_String 项目由 UTF-8 字符串表示。当加载类时,UTF-8 字符串会按需转换为 java.lang.String 对象。这可能会浪费内存,因为每个内部化字符串中的每个字符占用 3 个或更多字节(字符串中的 2 个字节,UTF-8 中的 1-3 个字节)。

而且,由于字符串是动态创建的,它们不能轻松地在 JVM 进程之间共享。

描述

在转储时,在堆初始化期间在 Java 堆内分配了一个指定的字符串空间。在写出内部化字符串表和 String 对象时,修改对应的指向内部化 String 对象和底层 char 数组对象的指针,就好像这些对象来自指定的空间一样。

字符串表在转储时进行压缩,然后存储在存档中。字符串表的压缩技术与共享符号表相同(参见 JDK-8059510)。使用常规的窄 oop 编码和解码来访问压缩字符串表中的共享 String 对象。

在具有压缩 oop 指针的 64 位平台上,使用偏移量(带或不带缩放)从窄 oop 基址编码窄 oop。目前有四种不同的编码模式:32 位未缩放模式、零基址模式、不连续堆基址模式和堆基址模式。根据堆大小和堆最小基址,选择适当的编码模式。窄 oop 的编码模式(包括编码位移)必须在转储时和运行时保持一致,以便共享字符串空间内的 oop 指针在运行时仍然有效。在运行时,共享字符串空间可以被视为可重定位的,但有一些限制。它不需要在转储时与相同的地址映射,但是它应该与窄 oop 基址相同的偏移量在转储时和运行时。堆的大小在转储时和运行时不需要相同,只要使用相同的编码模式即可。字符串空间的偏移量和 oop 编码模式(以及移位)应存储在存档中以供运行时验证。如果编码模式发生变化,将使得对每个共享 Stringchar 数组的 oop 指针的编码无效。在这种情况下,虚拟机将忽略共享字符串数据,但仍然可以使用其他共享数据。虚拟机会报告一个警告,指示由于不兼容的 GC 配置而未使用共享字符串。

在运行时,字符串空间作为 Java 堆的一部分映射到与转储时相同偏移量的 oop 编码基址。映射从存档中保存的字符串空间的最低页面对齐地址开始。映射的字符串空间包含共享的 Stringchar 数组对象。与此映射空间重叠的所有 G1 区域都将被标记为固定的;这些 G1 区域无法在运行时进行分配。在部分重叠的区域可能存在未使用的浪费空间,但最多只能有一个这样的区域,在映射的末尾。在字符串空间内,oop 指针不需要进行修补,因为使用相同的窄 oop 编码。共享字符串空间是可写的,但 GC 不应该写入空间中的 oop,以保持在不同进程之间的共享性。试图锁定其中一个共享字符串的应用程序,因此写入共享空间的应用程序将获得该页面的私有副本,因此失去了共享该特定页面的好处。这种情况很少见。

共享字符串表在运行时与常规字符串表是不同的。查找内部化字符串时会同时搜索这两个表。共享字符串表在运行时是一个只读表;不能向其添加或删除条目。

G1 字符串去重表是一个单独的哈希表,包含用于运行时去重的 char 数组。当字符串被内部化并添加到 StringTable 时,如果该表中不存在,则字符串将被去重,并且底层的 char 数组将被添加到去重表中。去重表不会存储到存档中。在 VM 启动时,使用共享字符串数据填充去重表。为了减少启动时间,这项工作在 G1StringDedupThread(在 G1StringDedupThread::run() 中,在 initialize_in_thread() 之后)中完成。共享字符串的哈希值在转储时预先计算并存储在字符串中,以避免去重代码在运行时写入哈希值。

测试

对于此功能的测试将涵盖以下方面:

  • 此功能的基本运行;
  • 与此功能不兼容的模式,如非 G1 GC 和未压缩的对象/类指针;
  • 转储时间和运行时间之间的普通对象指针编码的变化;
  • 无效的字符串文件格式;
  • 使用此功能时选定的字符串操作,如内部化和字符串比较;
  • 确保此功能不会在使用 GC 诊断模式时导致堆破坏。

依赖关系

服务性代理需要更新以支持共享字符串表(参见 JDK-8079830)。

根据 JDK-8054307 提出的更改,底层的 char 数组将被更改为 byte 数组。将复制内部化字符串到字符串空间并执行去重的代码需要反映这一点,如果 JDK-8054307 被整合进来的话。这个影响应该是最小的。