Hotspot 中的 CDS 和 AppCDS

CDS 是一种易于使用且强大的 HotSpot JVM 功能,可以帮助提高启动性能。在本文中,我们将了解 CDS 的背景、架构以及如何使用和调试 CDS。

什么是 CDS?

CDS 是在 JDK 5 更新版本中添加的 HotSpot JVM 功能,作为 JDK 5 的一部分。它在随后的 JDK 版本中得到了增强和扩展。CDS 的目标是通过从预处理的 Java 类和 JVM 元数据存档中加载来减少 JVM 的启动时间,这些存档在初始化过程中使用。

本文将介绍 CDS 的优势、工作原理、如何在 Java 应用程序中使用 CDS 以及如何调试和排查 CDS 故障。

JVM 初始化

在初始化期间,HotSpot JVM 必须加载并初始化一组核心类,例如,位于 java.lang 包中的许多类。无论在 JVM 上运行哪些应用程序,此过程变化不大。此重复过程代表了一个优化机会。

从 JDK 5 开始,可以使用 -Xshare:dump 命令创建 HotSpot 通常在启动时需要加载的核心类的预处理存档。此共享存档位于:$JAVA_HOME/lib/server/classes.jsa(Windows:$JAVA_HOME/bin/server/classes.jsa)。

在初始化时,HotSpot JVM(如果指示)将在该目录中搜索共享存档,如果找到,则将存档加载到只读内存映射位置。这节省了时间,因为加载预处理存档比加载类更快,因为跳过了诸如:从存档格式解压缩文件、验证文件和生成字节码之类的步骤。

默认 CDS 存档

在 JDK 12 版本中,为 JDK 映像的 64 位版本提供了特定于体系结构的默认 CDS 存档。这避免了运行 -Xshare:dump 命令以利用以前版本的 JDK 所需的 CDS 的要求。默认 CDS 存档位于:$JAVA_HOME/lib/server/classes.jsa(Windows:$JAVA_HOME/bin/server/classes.jsa)。

关于“类数据共享”

从历史上看,“CDS”是 Class Data-Sharing 的缩写,并且在有关 CDS 的文档中仍然经常使用。这个名称与预处理存档是如何内存映射并与同一台机器上运行的其他 JVM 进程共享相关的。这将减少 JVM 进程的整体内存占用。

CDS 的此功能不再被积极支持,因为内存成本已大幅下降,并且内存可用性已大幅提高。此外,在服务器上运行的应用程序的部署模型是在紧密配合的容器中,其中 JVM 是唯一运行的进程,这消除了共享内存映射的优势。随后,“CDS”不再代表 Class Data-Sharing。

使用 CDS

CDS 由 -Xshare:<value> 参数控制,该参数接受以下值;

  • auto : 启用 CDS 在存在共享存档时使用。默认注意:从 JDK 12 开始,auto 被设置为所有 64 位版本的默认值,并假设共享存档位于:$JAVA_HOME/lib/server/classes.jsa(Windows:$JAVA_HOME/bin/server/classes.jsa
  • on : 要求使用 CDS。如果 JVM 在尝试加载共享存档时遇到问题,JVM 将打印错误消息并退出。注意:仅应用于测试目的,切勿在生产环境中使用。
  • off : 禁用 CDS。
  • dump : 生成 CDS 存档。

CDS 的性能优势

使用简单的“Hello World”应用程序进行测试时,CDS 的性能提升约为 33%,如以下测试结果所示

$ time java -Xshare:off HelloWorld
Hello world!
java -Xshare:off HelloWorld  0.08s
$ time java -Xshare:on HelloWorld
Hello world!
java -Xshare:on HelloWorld  0.05s

使用 -Xshare:off 时,应用程序的执行时间为 0.10 秒,而使用 -Xshare:on 时为 0.05 秒。

仅对核心 JDK 类使用 CDS 的可衡量性能优势随着启动的 Java 进程的大小和复杂性的增加而变得越来越小。这是引入 AppCDS 的动机之一。

AppCDS

AppCDS 作为 JDK 10 版本的一部分添加到 HotSpot JVM 中,并使用 JEP 310。AppCDS 旨在将 CDS 的优势扩展到包括应用程序类。AppCDS 已经过进一步的增强,包括 JDK 13 和 19 版本中的主要增强,这些增强提高了其性能和易用性。AppCDS 允许随着 Java 应用程序的大小和复杂性的增长,更一致地使用 CDS 的优势。

AppCDS 支持以下位置

  • 来自运行时映像的平台类
  • 来自运行时映像的应用程序类
  • 来自类路径的应用程序类
  • 来自模块路径的应用程序类

使用 AppCDS

由于 CDS 仅涵盖 JDK 中包含的核心 Java 类,因此可以将预处理的共享存档作为 JDK 的一部分包含在内。这允许开发人员在无需任何进一步交互的情况下开箱即用地使用 CDS(对于从 JDK 12 开始的所有平台)。但是,要使用 AppCDS,开发人员需要进行一些主动干预。

生成动态共享存档

在 JDK 13 中添加的动态共享存档功能旨在简化大多数用例中 AppCDS 的使用,并为使用用户定义类加载器的应用程序提供更好的支持。要生成动态共享存档,您需要使用 -XX:ArchiveClassesAtExit=<存档文件名称> 命令,该命令将在应用程序退出时生成共享存档。使用此命令的具体示例如下

java -XX:ArchiveClassesAtExit=petclinic-dynamic-archive.jsa -jar target/spring-petclinic-2.5.1.jar

注意:生成共享存档将在 JVM 初始化和终止过程中产生重大性能影响。

要在后续启动中使用生成的存档,需要使用 -XX:SharedArchiveFile=<存档文件名称>。使用先前示例中生成的存档将如下所示

java -XX:SharedArchiveFile=petclinic-dynamic-archive.jsa  -jar target/spring-petclinic-2.5.1.jar
没有默认存档的动态存档

生成动态存档时,它将不包括默认存档中包含的核心类。相反,动态存档将引用默认存档的默认位置。假设存档文件丢失或损坏。在这种情况下,JDK 核心类和元数据将正常加载,从而对启动性能产生负面影响(或者,如果使用 -Xshare:on,则系统将退出)。

自动生成动态存档

在 JDK 19 版本中,JDK-8261455 引入了 JVM 使用 -XX:+AutoCreateSharedArchive 自动生成共享存档的功能。此命令将告诉 JVM 查找使用 -XX:SharedArchiveFile 定义的共享存档;如果存档不存在或状态无效,则将生成存档。此功能的一个主要优势是,用于启动应用程序的相同 java 命令可以生成存档,从而减少维护工作。

如果现有存档已损坏、使用较旧的 JDK 版本生成或任何依赖的 JAR 已更改,则将生成一个新的存档。

静态存档

在大多数情况下,动态存档就足够了。但是,在某些情况下,静态存档可能更有利。一些这样的情况可能是

  • 存储额外的符号和字符串数据
  • 在某些情况下略微提高启动性能

要创建静态存档,首先,必须使用 -XX:DumpLoadedClassList=<类列表名称> 命令生成类列表。在此过程中,还必须关闭 CDS(-Xshare:off)。继续上面使用 Spring Boot Petclinic 应用程序的示例,要创建类列表,请运行以下命令

java -Xshare:off -XX:DumpLoadedClassList=petclinic.classlist -jar target/spring-petclinic-2.5.1.jar

接下来,必须使用 -XX:SharedArchiveFile=<存档文件名称> 命令与 -Xshare:dump 和先前步骤中生成的类列表 -XX:SharedClassListFile=<类列表名称> 一起生成共享存档,如以下示例所示

java -Xshare:dump -XX:SharedArchiveFile=petclinic-static-archive.jsa -XX:SharedClassListFile=petclinic.classlist

要在后续启动中使用生成的存档,需要使用 -XX:SharedArchiveFile=<存档文件名称>。使用先前示例中生成的存档将如下所示

java -XX:SharedArchiveFile=petclinic-static-archive.jsa -jar target/spring-petclinic-2.5.1.jar
共享类的静态存档

以下是如何使用静态存档功能创建共享类/库的存档的示例,这些类/库在多个 Java 应用程序中使用

要包含来自 hello.jar 和 hi.jar 的类,必须将 .jar 文件添加到由 -cp 参数指定的类路径中。

创建 Hello 应用程序使用的所有类的列表,以及 Hi 应用程序的另一个列表

   java -XX:DumpLoadedClassList=hello.classlist -cp common.jar:hello.jar Hello

 

   java -XX:DumpLoadedClassList=hi.classlist -cp common.jar:hi.jar Hi

创建所有应用程序使用的单个类列表,这些应用程序将共享共享存档文件。

Linux 和 macOS 以下命令将文件 hello.classlist 和 hi.classlist 合并到一个文件 common.classlist 中

   cat hello.classlist hi.classlist > common.classlist

Windows 以下命令将文件 hello.classlist 和 hi.classlist 合并到一个文件 common.classlist 中

   type hello.classlist hi.classlist > common.classlist

创建一个名为 common.jsa 的共享存档,其中包含 common.classlist 中的所有类

   java -Xshare:dump -XX:SharedArchiveFile=common.jsa -XX:SharedClassListFile=common.classlist -cp common.jar:hello.jar:hi.jar

使用的类路径参数是 Hello 和 Hi 应用程序共享的公共类路径前缀。

使用相同的共享存档运行 Hello 和 Hi 应用程序

   java -XX:SharedArchiveFile=common.jsa -cp common.jar:hello.jar:hi.jar Hello
   java -XX:SharedArchiveFile=common.jsa -cp common.jar:hello.jar:hi.jar Hi

来源

带有默认 CDS 存档的静态存档

与动态归档不同,静态归档确实包含核心 JDK 类。因此,如果系统上的默认 CDS 归档已被删除或损坏,这不会影响在启动时引用静态归档的 JVM 进程。

 

调试 CDS

CDS 旨在成为故障安全的,并且应该在后台无缝地工作。CDS 在使用默认设置 -Xshare:auto 时,如果共享归档丢失、损坏或无效,应该回退到从文件系统加载类。

但是,在某些情况下,CDS 确实会导致 JVM 在启动时崩溃。本节将介绍导致 CDS 失败的常见问题,以及在尝试创建 CDS 归档时可能出现的问题以及如何诊断和调试这些问题。

错误消息

最容易调试的形式是在 CDS 在启动时遇到问题导致 JVM 退出时。这应该只在使用 -Xshare:on 时发生。如果 JVM 在尝试加载共享归档时遇到错误,而此时设置了 -Xshare:auto(默认值),则 JVM 将静默忽略错误并正常加载类。因此,不建议在生产环境中使用 -Xshare:on

无效或丢失的共享归档

如果找不到由 -XX:SharedArchiveFile 定义的共享归档,HotSpot JVM 将在控制台中打印此消息

An error has occurred while processing the shared archive file.
Specified shared archive not found (<name of the archive>).
Error occurred during initialization of VM

如果由 -XX:SharedArchiveFile 定义的共享归档已损坏或无效,HotSpot JVM 将在控制台中打印此消息

An error has occurred while processing the shared archive file.
The shared archive file has a bad magic number.
Error occurred during initialization of VM
Unable to use shared archive.

如果默认 CDS 归档文件丢失或损坏,HotSpot JVM 将在控制台中打印此消息

An error has occurred while processing the shared archive file.
Specified shared archive not found (<JAVA_HOME>/Contents/Home/lib/server/classes.jsa).
Error occurred during initialization of VM
Unable to use shared archive.

无效或丢失的类列表

在生成静态共享归档时,如果找不到由 -XX: SharedClassListFile 定义的类列表,将打印以下错误

Error occurred during initialization of VM
Loading classlist failed: No such file or directory

在生成静态共享归档时,如果由 -XX: SharedClassListFile 定义的类列表已损坏,将打印以下错误

An error has occurred while processing class list file HelloMessage-test.classlist 1:9.
Unknown input:
Invalid format
        ^
Error occurred during initialization of VM
class list format error.

归档生成报告

在生成共享归档时,JVM 进程默认情况下会将大量诊断信息打印到控制台。此信息可用于查看哪些类和库正在被添加到共享归档中,哪些没有被添加到共享归档中。

正在添加到共享归档中的类/库

[0.007s][info][class,load] java.lang.Object source: jrt:/java.base
[0.007s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.007s][info][class,load] java.lang.Comparable source: jrt:/java.base

无法添加到共享归档中的类/库

[14.078s][warning][cds] Pre JDK 6 class not supported by CDS: 49.0 jdk/internal/reflect/GeneratedMethodAccessor55
[14.078s][warning][cds] Skipping org/springframework/beans/NotReadablePropertyException: Not linked

生成的类列表

在生成静态归档时,可以检查类列表文件以查看哪些类和其他 JVM 元数据将被添加到共享归档中,如生成文件顶部的注释中所述,此文件不应手动修改。以下是一个类列表文件示例

# NOTE: Do not modify this file.
#
# This file is generated via the -XX:DumpLoadedClassList=<class_list_file> option
# and is used at CDS archive dump time (see -Xshare:dump).
#
java/lang/Object
java/io/Serializable
java/lang/Comparable
java/lang/CharSequence
java/lang/constant/Constable
java/lang/constant/ConstantDesc
java/lang/String
java/lang/reflect/AnnotatedElement
java/lang/reflect/GenericDeclaration
java/lang/reflect/Type
java/lang/invoke/TypeDescriptor
java/lang/invoke/TypeDescriptor$OfField

调试日志

有几个选项可以配置 HotSpot 以将其他日志输出到控制台,以便更好地详细说明 CDS 的内部行为。

调试 CDS 日志

  • -Xlog:cds=debug:可以在生成和加载共享归档期间使用,以提供有关正在添加到共享归档中的类和附加元数据的详细统计信息,或者在

  • -Xlog:cds+lambda=debug:与 -Xlog:cds=debug 一样,可以在归档生成和加载时使用。此选项提供了有关 CDS 处理 lambda 的更多信息。

调试类加载日志

在加载共享归档时,JVM 参数 -verbose:class 可用于查看哪些类正在从共享归档中加载,或者通过 HotSpot 的正常类加载过程加载。如果一个类正在从共享归档中加载,它将报告其来源为 shared objects file。例如,以下示例输出

[0.008s][info][class,load] java.lang.Object source: shared objects file
[0.009s][info][class,load] java.io.Serializable source: shared objects file

如果类没有从共享归档中加载,它将报告其源位置,例如,java.lang.Objectjava.io.Serializable 从模块 jrt:/java.base 加载

[0.007s][info][class,load] java.lang.Object source: jrt:/java.base
[0.007s][info][class,load] java.io.Serializable source: jrt:/java.base

更多学习

最后更新: 2021 年 9 月 14 日


返回教程列表