0.前言
本文的目的是详细讨论 HotSpot JVM 自 JDK 1.5 以来提供的一项功能,该功能可以减少启动时间,但如果在多个 JVM 之间共享相同的类数据共享 (CDS) 存档,则还可以减少内存占用。
1.类数据共享 (CDS)
CDS 的想法是使用特定格式将预处理的类元数据缓存在磁盘上的存档中,以便可以非常快速地加载它们(与从 JAR 文件存储和加载的类相比会快很多)。 CDS 是在 Sun JDK 1.5 中引入的,最初仅适用于 Java HotSpot 客户端 VM 和串行垃圾收集器 (GC)。 在 JDK 9 中,它被扩展为支持 C2 Just-inTime Compiler (JIT) 和其他收集器(例如 Parallel、ParallelOld 和 G1)。 从 JDK 17 开始,它支持 ZGC、G1 GC、串行 GC、并行 GC 和 Shenandoah GC。
CDS 存档仅包含大多数应用程序使用的核心库类。 这些类(从 JDK 17 开始,总共大约 1400 个)由引导类加载器加载,它们属于以下包:java.lang.、java.util.、java.io.、java.nio.、 jdk.internal.、java.security.、java.net.* 等。CDS 存档也称为默认 CDS 存档或静态基础 CDS 存档。
从 JDK 12 (JEP 341) 开始,CDS 存档是在 64 位平台上的 JDK 构建期间通过运行 -Xshare:dump 并使用 G1 GC(默认 GC)创建的。 它使用内置时间生成的默认类列表,并且根据平台位于不同的目录下。
Linux/macOS:
// default CDS archive
$JAVA_HOME/lib/server/classes.jsa
// class list
$JAVA_HOME/lib/classlist
Windows:
// default CDS archive
$JAVA_HOME\\bin\\server\\classes.jsa
// class list
$JAVA_HOME\\bin\\classlist
CDS存档分为7个区域,如下图所示:
说明:
- rw – 读写元数据(例如,C++ vtables)
- ro – 只读元数据和只读表(例如,SymbolTable、StringTable、SystemDictionary)
- bm – 标记存档内不同区域的所有指针位置的位图
- ao0 – 打开归档堆空间 0(例如,java 基本类型(例如,Boolean、Char、Float 等)、Klass* 对象(例如,InstanceKlass、TypeArrayKlass*、ObjArrayKlass*))
- ao1 – 打开归档堆空间 1(可能为空)
- ca0 – 封闭的归档堆空间 0(例如,内部字符串)
- ca1 – 封闭的归档堆空间 1(可能为空)
为了更深入地理解(或只是出于好奇),我建议查看 OpenJDK 源代码。
除非指定 -Xshare:off,否则在大多数 JDK 发行版中默认启用 CDS。 当 JVM 启动时,存档会进行内存映射并在多个 JVM 进程之间共享(使用共享文件系统)。 然而,目前只有具有 Compressed{Opps,ClassPointers} 的 G1 GC 可以在启动时映射存档堆区域。 使用不同的 GC(例如 SerialGC)启动 JVM 最终将禁用共享 Java 堆对象。 默认情况下,不会打印此信息,因此要获取它,您必须在启动应用程序时显式指定 -Xlog:cds 选项。
$ java -Xlog:cds -XX:+UseSerialGC -cp <app jar> MyApp
[info][cds] CDS heap data is being ignored. UseG1GC, UseCompressedOops and UseCompressedClassPointers are required.
2.归档在磁盘上的占用空间
存储在 CDS 中的类比存储在 JAR 文件或 JDK 运行时映像中的类大几倍(例如 3 – 5 倍)。 例如,JDK 17 中的默认类列表包含 1399 个类,存档总共约 13,372 KB (~ 13.05MB)。
$ cat $JAVA_HOME/lib/classlist | wc -l
1399
$ ls -l --block-size=1K $JAVA_HOME/lib/server/
-r--r--r-- 1 10668 10668 13372 Dec 7 22:48 classes.jsa
3.应用程序类数据共享(AppCDS)
应用程序类数据共享 (AppCDS) 将 CDS 概念扩展到内置系统类加载器(即应用程序类加载器)和自定义类加载器。 这最初是作为商业 Oracle JDK 功能添加的,但后来成为 OpenJDK 10 (JEP 310) 的一部分。 AppCDS 也称为静态存档。
AppCDS 存档必须显式创建,过程分为三步。
步骤 1:创建 AppCDS 类列表(例如 static-cds.lst)。 可以进行多次试运行来创建此类列表。
$ java -Xshare:off -XX:DumpLoadedClassList=static-cds.lst -cp <app jar> MyApp
如果您打开类列表,您可能会注意到它还包括核心库类(它们是默认 CDS 存档的一部分)。
步骤 2:根据之前创建的类列表创建 AppCDS 存档(例如 static-cds.jsa)
$ java -Xshare:dump -XX:SharedClassListFile=static-cds.lst -XX:SharedArchiveFile=static-cds.jsa -cp <app jar> MyApp
步骤 3:启动应用程序并指定 AppCDS 存档的名称作为参数
$ java -XX:SharedArchiveFile=static-cds.jsa -cp <app jar> MyApp
4.共享基地址
默认情况下,在转储期间,存档会映射到共享基地址 0x800000000。 如果所需的地址空间不可用,地址空间布局随机化 (ASLR) 可能会导致此操作偶尔失败。 为了使 JVM 能够应对此故障,您可以考虑使用 -Xshare:auto 选项运行它,或者(如果这对您的设置有意义;例如,在基准测试期间)甚至禁用 ASLR。
无论哪种方式,在 -Xshare:dump 期间,选项 -XX:SharedBaseAddress=<new_address> 可用于覆盖默认共享基地址或 -XX:SharedBaseAddress=0 以映射到操作系统选定的地址。
将 -Xlog:cds 选项添加到之前步骤 2 中的命令(即创建 AppCDS 存档)会打印存档区域及其基地址:
$ java -Xlog:cds -Xshare:dump -XX:SharedClassListFile=static-cds.lst -XX:SharedArchiveFile=static-cds.jsa -cp <app jar> MyApp
[info][cds] Dumping shared data to file:
[info][cds] static-cds.jsa
[info][cds] Shared file region (rw ) 0: 8093376 bytes, addr 0x0000000800000000 file offset 0x00001000 crc 0x5b23ef23
[info][cds] Shared file region (ro ) 1: 13016776 bytes, addr 0x00000008007b8000 file offset 0x007b9000 crc 0x12a5a9d7
[info][cds] Shared file region (bm ) 2: 381440 bytes, addr 0x0000000000000000 file offset 0x01423000 crc 0x80f2eed3
[info][cds] Shared file region (ca0) 3: 925696 bytes, addr 0x00000007bfc00000 file offset 0x01481000 crc 0x0e32d31c
[info][cds] Shared file region (oa0) 5: 724992 bytes, addr 0x00000007bf800000 file offset 0x01563000 crc 0xc4b5b70d
5.存储临时字符串
如果您正在使用 AppCDS,那么您可能还会有兴趣了解如何使用字符串数据和符号数据增强存档 (JEP 250)。 这可能会进一步减少应用程序的启动时间,特别是在应用程序使用大量字符串的情况下。 然而,创建额外的共享配置文件(包含字符串和符号)并不简单,因此我将尝试在这里简要解释一下。
字符串数据和符号数据可以使用附加到正在运行的 JVM 进程的 jcmd 工具生成:
$ jcmd <PID> VM.stringtable -verbose
$ jcmd <PID> VM.symboltable -verbose
然后,输出必须合并到单个文件(例如 static-cds-shared-strings.cfg),其整体结构如下:
VERSION: 1.0
@SECTION: String
$ jcmd <pid> VM.stringtable -verbose
@SECTION: Symbol
$ jcmd <pid> VM.symboltable -verbose
OpenJDK 源代码中提供了一个示例(用于测试目的)(例如,如果您想查看整体结构)。 Volker Simonis 在他的演讲中详细介绍了相同的功能:HotSpot VM 中的数据共享类。
要使用附加共享配置文件(包括字符串和符号数据)创建 AppCDS 存档,您需要使用以下参数列表启动应用程序:
$ java -Xshare:dump -XX:SharedClassListFile=static-cds.lst -XX:SharedArchiveConfigFile=static-cds-shared-strings.cfg -XX:SharedArchiveFile=static-cds.jsa -cp <app jar> MyApp
这与上面的步骤 3 非常相似,但还使用了 -XX:SharedArchiveConfigFile。
6.动态类数据共享(动态CDS)
动态 CDS 进一步扩展了 AppCDS,以动态允许在 Java 进程结束时进行归档。 该存档也简称为动态存档。 此功能自版本 13 (JEP 350) 起成为 OpenJDK 的一部分
动态 CDS 无需创建类列表(即初始 AppCDS 步骤),从而简化了 AppCDS 存档创建,因此它是一个两步过程。
第 1 步:创建动态 CDS 存档
$ java -XX:ArchiveClassesAtExit=dynamic-cds.jsa -cp <app jar> MyApp
步骤 2:启动应用程序并指定动态 CDS 存档的名称作为参数
$ java -XX:SharedArchiveFile=dynamic-cds.jsa -cp <app jar> MyApp
7.基础层依赖
动态 CDS 存档(隐式)创建在静态基本 CDS 存档(例如,classes.jsa)之上作为顶层存档,并且它使用更少的磁盘空间(因为核心库类不是其中的一部分)。 使用 -Xlog:cds 选项启动应用程序会打印两个存档:
$ java -Xlog:cds -XX:SharedArchiveFile=dynamic-cds.jsa -cp <app jar> MyApp
[info][cds] trying to map $JAVA_HOME/lib/server/classes.jsa
[info][cds] Opened archive $JAVA_HOME/lib/server/classes.jsa
[info][cds] trying to map dynamic-cds.jsa
[info][cds] Opened archive dynamic-cds.jsa
动态CDS存档和静态存档之间的分层依赖关系可以说明如下:
在此依赖关系链中,静态存档可以是默认 CDS 存档(即,classes.jsa)或自定义 AppCDS 存档(即,静态存档)。 用作基础层存档的 AppCDS 会覆盖默认的 CDS 存档。 动态存档仅提供可在 AppCDS 存档中的类之上加载的附加类。
当您拥有所有应用程序通用的同一组库(例如框架库)时,将 AppCDS 作为基础层中的静态存档可能会很有帮助。 此外,每个应用程序的详细信息都作为顶层存档转储在动态存档中。
8.基于AppCDS存档创建动态CDS存档
要在 AppCDS 存档之上创建动态 CDS 存档(作为非默认静态 CDS),您必须使用以下命令启动 JVM:
$ java -XX:SharedArchiveFile=static-cds.jsa -XX:ArchiveClassesAtExit=dynamic-cds.jsa -cp <app jar> MyApp
这与上面的步骤 2 非常相似,但另外 -XX:SharedArchiveFile 选项用于指定 AppCDS 存档。
9.在同一命令行中链接动态 CDS 存档和 AppCDS 存档
还可以选择在同一命令行中链接 AppCDS 和动态 CDS 存档:
$ java -XX:SharedArchiveFile=static-cds.jsa:dynamic-cds.jsa -cp <app jar> MyApp
注意:Windows 上的分隔符是 ; (反斜杠分号)而不是:(冒号)
HotSpot 不支持两个以上的存档。
使用 -Xlog:cds 选项启动应用程序会打印两个存档:
$ java -Xlog:cds -XX:SharedArchiveFile=static-cds.jsa:dynamic-cds.jsa -cp <app jar> MyApp
[info][cds] trying to map static-cds.jsa
[info][cds] Opened archive static-cds.jsa.
[info][cds] trying to map dynamic-cds.jsa
[info][cds] Opened archive dynamic-cds.jsa.
10.使用 jcmd 创建应用程序/动态 CDS 存档
到目前为止,我们在前两节中已经看到,要创建 AppCDS 或动态 CDS 存档,需要增强应用程序启动脚本(使用其他 JVM 选项),并且需要多次重新启动应用程序。 在本节中,我将介绍一种使用 jcmd 工具创建静态存档和动态存档的简化方法。
首先,启动应用程序:
$ java -cp <app jar> MyApp
其次,在应用程序运行时使用 jcmd 转储档案:
$ jcmd <PID> VM.cds static_dump static-cds.jsa
$ jcmd <PID> VM.cds dynamic_dump dynamic-cds.jsa
注意:为了能够转储动态存档,与 对应的 JVM 进程需要在启动应用程序时(在第一步中)指定一个附加选项 - XX:+RecordDynamicDumpInfo。
11.不足和建议
使用与创建时不同的 JDK 版本运行 CDS 存档不起作用(即升级 JDK 版本而不重新生成存档)。 JDK 18、19(JDK-8272331、JDK-8261455)中已解决此问题。 例如,以下存档是使用 JDK 17 创建并使用 JDK 18 启动的。
$ java -Xlog:cds -XX:SharedArchiveFile=dynamic-cds.jsa -cp <app jar> MyApp
[info][cds] Opening shared archive: dynamic-cds.jsa
[info][cds] UseSharedSpaces: Cannot handle shared archive file version 11. Must be at least 12
[info][cds] Unable to use shared archive: invalid archive
CDS 存档不可跨平台重复使用(例如 Linux、Windows、macOS)。 例如,以下存档是在 Linux 上创建并在 Windows 上启动的(即使使用相同的 JDK 版本)。
$ java -Xlog:cds -XX:SharedArchiveFile=dynamic-cds.jsa -cp <app jar> MyApp
[info][cds] trying to map dynamic-cds.jsa
[info][cds] Opened archive dynamic-cds.jsa.
[info][cds] _jvm_ident expected: OpenJDK 64-Bit Server VM (17.0.2+8-86) for windows-amd64 JRE (17.0.2+8-86), built on Dec 7 2021 21:49:10 by "mach5one" with MS VC++ 16.8 / 16.9 (VS2019)
[info][cds] actual: OpenJDK 64-Bit Server VM (17.0.2+8-86) for linux-amd64 JRE (17.0.2+8-86), built on Dec 7 2021 21:41:21 by "mach5one" with gcc 10.3.0
[info][cds] UseSharedSpaces: The shared archive file was created by a different version or build of HotSpot
[info][cds] UseSharedSpaces: Unable to map shared spaces
生成存档后,在类路径或模块路径中使用修改后的 jar 时间戳运行 CDS 存档不起作用(即禁用动态存档,仅使用基础层存档)。 这意味着重新编译类并重新创建 jar(即使源 Java 类相同、相同的工件 id 和组 id)是不可能的。
$ java -Xlog:cds -XX:SharedArchiveFile=dynamic-cds.jsa -cp <app jar> MyApp
[info][cds] trying to map /usr/lib/jvm/openjdk-17.0.2/lib/server/classes.jsa
[info][cds] Opened archive /usr/lib/jvm/openjdk-17.0.2/lib/server/classes.jsa.
[info][cds] trying to map dynamic-cds.jsa
[info][cds] Opened archive dynamic-cds.jsa.
[info][cds] Reserved archive_space_rs [0x0000000800000000 - 0x0000000804400000] (71303168) bytes
[info][cds] Reserved class_space_rs [0x0000000804400000 - 0x0000000844400000] (1073741824) bytes
[info][cds] Mapped static region #0 at base 0x0000000800000000 top 0x0000000800457000 (ReadWrite)
[info][cds] Mapped static region #1 at base 0x0000000800457000 top 0x0000000800bde000 (ReadOnly)
[info][cds] Mapped dynamic region #0 at base 0x0000000800bde000 top 0x00000008021aa000 (ReadWrite)
[info][cds] Mapped dynamic region #1 at base 0x00000008021aa000 top 0x0000000804130000 (ReadOnly)
[info][cds] UseSharedSpaces: A jar file is not the one used while building the shared archive file: target/my-app-0.0.1-SNAPSHOT.jar
[info][cds] Unmapping region #0 at base 0x0000000800bde000 (ReadWrite)
[info][cds] Unmapping region #1 at base 0x00000008021aa000 (ReadOnly)
[warning][cds,dynamic] Unable to use shared archive. The top archive failed to load: dynamic-cds.jsa
CDS 存档不包括 JDK 5/6 之前的类(JDK-8202556、JDK-8230413)。 例如,以下输出是在启用 -Xlog:cds 选项的情况下生成动态存档的结果。
$ java -Xlog:cds -XX:ArchiveClassesAtExit=dynamic-cds.jsa -cp <app jar> MyApp
[warning][cds] Pre JDK 6 class not supported by CDS: 49.0 jdk/internal/reflect/GeneratedConstructorAccessor30
[warning][cds] Pre JDK 6 class not supported by CDS: 49.0 org/springframework/cglib/core/internal/Function
[warning][cds] Pre JDK 6 class not supported by CDS: 49.0 net/bytebuddy/dynamic/scaffold/MethodRegistry$Compiled
[warning][cds] Pre JDK 6 class not supported by CDS: 46.0 antlr/collections/impl/ASTArray
如果指定了 --upgrade-module-path、–patch-module 或 --limit-modules 选项,则 CDS 将被禁用。
$ java -Xlog:cds -XX:SharedArchiveFile=dynamic-cds.jsa --upgrade-module-path=target/modules -cp <app jar> MyApp
[0.000s][info][cds] optimized module handling: disabled due to incompatible property: jdk.module.upgrade.path=target/modules
创建存档时使用的类路径必须与运行时使用的类路径相同(或其前缀)。 模块路径不遵循相同的限制。
应用程序/动态 CDS 不包括其他 jar 作为类路径属性引用的 jar。
动态 CDS 档案应该在更广泛地使用应用程序(涵盖应用程序中的不同业务流程)之后创建,而不是通过启动并立即停止应用程序(即延迟加载类)来创建。
JDK版本使用越新越好。 最新的 JDK 版本包括显着的 CDS 改进或错误修复,例如:
- JDK 15 – 尝试在动态 CDS 转储期间链接所有类(即未链接)(JDK-8232081)
- JDK 15 – 支持动态 CDS 存档中的 Lambda 代理类 (JDK-8198698)
- JDK 15 – ZGC(生产就绪)支持 CDS(ZGC Main)
- JDK 16 – 支持静态 CDS 存档中的 Lambda 代理类 (JDK-8247666)
- JDK 17 – 将旧的类文件存储在静态 CDS 存档中 (JDK-8261090)
12.总结
在我看来,AppCDS 或动态 CDS 是您应该自己尝试的功能。 这是一种几乎可以免费带来好处的机制,您不必更改应用程序代码。 这些改进有多大,我无法告诉你,这取决于具体情况。
我最近在 Java 会议上提出了类似的主题(例如 CDS、AppCDS 和动态 CDS)。 您可以下载幻灯片,此外 GitHub 上还有一个简短的教程(包括一些命令行选项和我使用的应用程序)。