跳到主要内容

JEP 250:在 CDS 存档中存储驻留字符串

QWen Max 中英对照

概述

在类数据共享(CDS)存档中存储内部字符串。

目标

  • 通过在不同的 JVM 进程之间共享 String 对象和底层的 char 数组对象来减少内存消耗。

  • 仅支持 G1 GC 的共享字符串。共享字符串需要一个固定区域,而 G1 是唯一支持固定区域的 HotSpot GC。

  • 仅支持具有压缩对象和类指针的 64 位平台。

  • 在启动时间、字符串查找时间、GC 暂停时间或使用常规基准测试的运行时性能方面没有显著下降(< 2-3%)。

非目标

  • 减少启动时间不是目标。

  • 不会支持 G1 以外的其他类型的 GC。

  • 不会支持 32 位平台。

动机

目前,当 CDS(Class Data Sharing)将类存储到归档文件中时,常量池中的 CONSTANT_String 项是以 UTF-8 字符串的形式表示的。当类被加载时,这些 UTF-8 字符串会根据需要转换为 java.lang.String 对象。这种方式可能会浪费内存,因为在每个驻留字符串中,每个字符都会占用三字节或更多的空间(在 String 中占两字节,在 UTF-8 中占 1-3 字节)。

此外,由于字符串是动态创建的,因此无法轻松地在 JVM 进程之间共享。

描述

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

字符串表在转储时会被压缩并存储到存档中。字符串表的压缩技术与共享符号表相同(参见 JDK-8059510)。常规的窄 oop 编码和解码用于从压缩字符串表访问共享的 String 对象。

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

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

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

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

测试

此功能的测试将涵盖以下领域:

  • 此功能的基本操作;

  • 与此功能不兼容的模式,例如非 G1 GC 和未压缩的对象/类指针;

  • 在转储时和运行时普通对象指针编码的变化;

  • 无效的字符串文件格式;

  • 使用此功能时选定的字符串操作,例如字符串驻留和字符串比较;以及

  • 确保此功能不会通过 GC 诊断模式导致堆损坏。

依赖

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

随着 JDK-8054307 提出的变更,底层的 char 数组将被改为 byte 数组。如果 JDK-8054307 被集成,复制内部字符串到字符串空间并执行去重的代码需要反映这一变化。影响应该很小。