
应用程序类数据共享(AppCDS)-JVM功能可加快启动速度并节省内存。 早在JDK 1.5(2004)中就在HotSpot中出现时 ,它长期以来仍然非常有限,甚至部分地商业化。 仅在OpenJDK 10(2018)中,它才可供凡人使用,同时扩大了范围。 最近发布的 Java 13试图简化此应用程序。
AppCDS的想法是在同一主机上的同一JVM实例之间“共享”已加载的类。 看来这对微服务特别有用,特别是Spring Boot上具有成千上万个库类的“ broilers”,因为现在这些类将不需要在每个JVM实例的每次启动时都进行加载(解析和验证),并且不会在内存中重复。 这意味着启动应该变得更快,并且内存消耗应该更低。 太好了,不是吗?
一切都是这样,一切都是如此。 但是,如果您(odnokhabryanin)过去不相信林荫大道的标志,而是相信特定的数字和示例,那么欢迎来到Kat –让我们尝试弄清楚它的真正含义是...
代替免责声明
在您之前,它不是使用AppCDS的指南,而是小研究结果的摘要。 我很想了解这个JVM函数如何在我的工作项目中应用,我试图从企业开发人员的角度对其进行评估,并在本文中阐述了结果。 其中不包括在模块路径上使用AppCDS,在其他虚拟机(不是HotSpot)上实现AppCDS以及使用容器的复杂性等主题。 但是,有一个理论部分用于探讨该主题,还有一个实验性部分,您可以自己重复体验。 尚未将任何结果应用到生产中,但谁知道明天会是...
理论
AppCDS简介
您可能在多个来源中都熟悉过此主题,例如:
- 在Nikolai Parlog的文章中 (包括Java 13包子,但没有Spring Boot)
- 在Volker Simonis的报告和文章中 (没有Java 13,但有详细信息)
- 这些行的作者在一份报告中 (没有Java 13,但重点是Spring Boot)
为了不进行转述,我仅强调对本文很重要的几点。
首先,AppCDS是CDS功能的扩展,该功能在HotSpot中早已出现,其实质如下:

为了使这两个想法都变为现实,您需要执行以下操作(一般而言):
- 获取要在应用程序实例之间共享的类的列表
- 将这些类合并到适合内存映射的存档中
- 启动时将存档连接到应用程序的每个实例
看来算法只是3个步骤-采取并执行。 但是,新闻开始了,各种各样的事情。
不好的是,在最坏的情况下,这些项目中的每一个都会变成至少一个具有自己特定选项的JVM启动,这意味着整个算法是对相同类型的选项和文件的微妙处理。 听起来不是很有希望,不是吗?
但是有个好消息:改进此算法的工作正在进行中 ,并且随着Java的每个发行版,其应用变得更加容易。 因此,例如:
- 在OpenJDK 10和11中,如果您只想共享主要的JDK类,则可以跳过第1步,因为它们已经为我们编译过,并放入
$JAVA_HOME\lib\classlist
(≈1200个)中。 - 在OpenJDK 12中,您可以跳过第2步 ,因为与类列表一起,发行档案还包括一个现成的档案,它们可以直接使用,并且不需要显式连接。
- 如果您想共享其他所有内容(通常只是想共享)
OpenJDK 13 提供了动态CDS存档-在应用程序运行期间收集的存档,并在配备人员后保存。 这使您可以将点1和2折叠为一个不太混乱的点(尽管并不是所有事情都那么简单,但稍后会介绍更多)。
因此,无论准备AppCDS的过程是什么,上面列出的3个步骤始终是紧随其后的,只是在某些情况下它们是被遮盖的。
您可能已经注意到,随着AppCDS的出现,许多应用程序类开始出现双重生活:它们同时生活在以前的位置(通常是JAR文件)和新的共享存档中。 同时,开发人员继续在同一位置更改/删除/补充它们,并且JVM在工作时从新的位置获取它们。 不必担心这种情况的危险:如果不采取任何措施,迟早该课程的副本将受到腐蚀,我们将获得典型的“ JAR地狱”的许多魅力。 显然,JVM无法阻止类更改,但是它应该能够及时检测到差异。 但是,通过成对比较类(甚至通过校验和)来做到这一点是一个想法。 它可以抵消其余的生产率提高。 这可能就是为什么JVM工程师没有选择单个类作为比较对象,而是选择整个类路径的原因,并在AppCDS文档中进行了说明:“创建共享存档时,类路径应与应用程序的后续启动相同(或至少是前缀)。”
请注意,在归档创建时使用的类路径必须与运行时使用的类路径相同(或前缀)。
但这并不是明确的声明,因为您记得,可以通过不同的方式来形成类路径,例如:
- 从已编译的软件包目录中读取裸露的
.class
文件,
例如java com.example.Main
- 使用通配符时,使用JAR文件扫描目录,
例如java -cp mydir/* com.example.Main
- 明确列出JAR和/或ZIP文件,
例如java -cp lib1.jar;lib2.jar com.example.Main
(这不包括还可以通过例如JVM选项-cp/-classpath/--class-path
, CLASSPATH
环境变量或要启动的Class-Path
JAR文件的属性来不同地设置类路径的事实)
在这些方法中,AppCDS仅支持一种方法-JAR文件的显式枚举。 显然,HotSpot JVM工程师认为,只有在尽可能明确地指定它们(使用通常的详尽列表)的情况下,比较AppCDS存档和已启动的应用程序中的类路径才足够快速和可靠。
CDS / AppCDS仅支持从JAR文件归档类。
在此必须注意,该语句不是递归的,即 不适用于JAR文件中的JAR文件(除非它与动态CDS有关,请参见下文)。 这意味着Spring Boot发行的普通JAR娃娃无法与常规AppCDS一起使用,您必须坐下来。
CDS工作中的另一个问题是共享归档文件被投影到具有固定地址的内存(通常从0x800000000
开始)。 这本身还不错,但是由于大多数操作系统默认情况下启用了地址空间布局随机化(ASLR),因此可能会部分占用所需的内存范围。 在这种情况下,HotSpot JVM的作用是特殊选项 -Xshare
,它支持三个值:
-Xshare:on
强制CDS / AppCDS; 如果范围繁忙,则JVM退出并显示错误。 不建议在生产环境中使用此模式,因为这会在启动应用程序时导致偶发的崩溃。-Xshare:off
-(您)切换CDS / AppCDS; 完全禁用共享数据的使用(包括嵌入式存档)-Xshare:auto
-JVM的默认行为,如果无法分配所需的内存范围,则-Xshare:auto
放弃并照常加载类
在撰写本文时,Oracle正在努力解决此类问题,但尚未分配发行号。
这些选项稍后对我们有用,但现在让我们看一下...
AppCDS应用
您可以通过多种方式使用AppCDS。 毁了你的生活 优化微服务的工作。 它们的复杂性和潜在利润差异很大,因此立即决定稍后再讨论哪个至关重要。
最简单的方法是不使用AppCDS,而仅使用CDS-这是只有平台类进入共享档案库的时候(请参阅“ AppCDS简介”)。 我们将立即删除此选项,因为在Spring Boot上应用于微服务时,它带来的利润太少。 这可以通过使用一个真正的微服务的示例(参见绿色部分)通过共享类的数量在其一般分布中的比例来看出:

更复杂,但很有前途的是使用成熟的AppCDS,即在同一个归档文件中同时包含库和应用程序类。 这是一整套选项,它们是从参与的应用程序数量和实例数量的组合中得出的。 以下是作者对AppCDS各种应用程序的好处和困难的主观评估。
注意事项:
- 在一个实例中的一个应用程序中(第1个),内存利润可能变为零甚至为负(尤其是在Windows下测量时)
- 创建正确的共享存档需要采取措施,其复杂程度并不取决于应用程序随后将启动多少个副本(比较选项对1-2和3-4)。
- 同时,从一个实例到多个实例的过渡显然会增加两个指标的利润,但不会影响准备工作的复杂性。
在本文中,我们将仅提供选项2 (通过选项 1),因为它足够简单,可以与AppCDS熟识,并且只有简单的操作,我们才能使用最近发布的JEP-350动态CDS存档,我想在实际中使用它。
动态CDS档案
JEP-350动态CDS存档是Java 13的主要创新之一,旨在使AppCDS易于使用。 要感到简化,您必须首先了解复杂性。 让我提醒您,AppCDS的经典“干净”应用程序算法包括3个步骤:(1)获取共享类的列表,(2)从它们中创建一个存档,以及(3)在连接了存档的情况下运行该应用程序。 在这些步骤中,只有第3个实际上有用,其余的只是为此做准备。 尽管获得类列表(步骤1)似乎非常简单(在某些情况下甚至没有必要),但实际上,在处理非平凡的应用程序时,事实证明这是最困难的,尤其是对于Spring Boot。 因此,仅JEP-350就是为了消除这一步骤,或者使其自动化。 想法是,JVM本身会绘制应用程序所需的类的列表,然后由它们自己形成所谓的“动态”归档。 同意,听起来不错。 但是要注意的是,现在尚不清楚何时停止累积类,然后继续将其放入存档中。 以前,在经典的AppCDS中,我们自己选择了此刻,甚至可以在这些操作之间切换以更改类列表中的某些内容,然后再将其转换为存档。 现在,这是自动发生的,并且仅在瞬间,JVM工程师为此选择了唯一的折衷方案-定期关闭JVM。 这意味着在应用程序停止之前将不会创建存档。 该解决方案有两个重要的后果:
- 在JVM崩溃的情况下,无论届时累积的类列表有多美妙,都不会创建归档文件(您以后无法使用常规方法将其提取出来)。
- 仅从那些在应用程序会话期间成功加载的类创建存档。 对于Web应用程序,这意味着从头开始和停止创建归档文件是不正确的,因为那样的话,许多重要的类都不会进入归档文件。 必须至少对应用程序执行一个HTTP请求(最好在所有情况下正确运行它),以便加载其实际使用的所有类。
动态和静态归档之间的一个重要区别是,它们总是构成基本静态归档的“附加组件”,基本归档可以是内置在Java分发工具包中的归档,也可以以经典的三步方式单独创建。
从语法上讲,使用动态CDS存档归结为两个带有两个选项的JVM启动:
- 使用
-XX:ArchiveClassesAtExit=archive.jsa
选项进行试运行-XX:ArchiveClassesAtExit=archive.jsa
,最后将创建一个动态档案(您可以指定任何路径和名称) - 使用
-XX:SharedArchiveFile=archive.jsa
选项的有用启动-XX:SharedArchiveFile=archive.jsa
,它将使用先前创建的存档
第二个选项与连接常规静态存档没有什么不同。 但是,如果基本静态存档突然不在默认位置(在JDK内),则此选项可能还包括指向其的路径的指示,例如:
-XX:SharedArchiveFile=base.jsa:dynamic.jsa
(在Windows中,路径分隔符必须为“;”字符)
现在您已经对AppCDS了如指掌,因此可以实际使用它。
练习
实验兔
因此,我们在实践中对AppCDS的应用不仅限于典型的HelloWorld,我们将以Spring Boot上的实际应用为基础。 我和我的同事们经常不得不在远程测试服务器上观看应用程序日志,并观看“实时”日志,就像编写它们一样。 为此,使用成熟的日志聚合器(例如ELK)通常是不合适的。 无休止地下载日志文件-很长时间了,看着tail
的灰色控制台输出令人沮丧。 因此,我制作了一个Web应用程序,该应用程序可以将任何日志实时直接直接输出到浏览器,按重要性级别为行着色(同时格式化XML),将多个日志聚合为一个,以及其他技巧。 它称为ANALOG (例如“ log分析器”,尽管并非如此),位于GitHub上 。 单击屏幕截图放大:

从技术上讲,这是Spring Boot + Spring Integration上的一个应用程序,在tail
, kubectl
和kubectl
支持下kubectl
(分别支持来自文件,Docker容器和Kubernetes资源的日志)。 它以经典的“厚” Spring Boot JAR文件的形式出现。 在运行时, ≈10K类挂在应用程序内存中,其中绝大多数是Spring和JDK类。 显然,这些类很少更改,这意味着可以将它们放入共享存档中,并在应用程序的所有实例中重用,从而节省内存和CPU。
单项实验
现在,让我们将Dynamic AppCDS的现有知识应用于实验兔子。 由于所有内容都是比较已知的,因此我们需要一些参考点-程序的状态,我们将与该状态比较实验期间获得的结果。
引言
- 所有其他命令均适用于Linux。 Windows和macOS的差异不是根本。
- JIT编译会显着影响结果,并且从理论上讲,为了保证实验的纯洁性,可以将其关闭(使用
-Xint
选项,如上述文章所述 ),但是出于最大可信度的考虑,决定不这样做。 - 在快速测试服务器上获得了以下有关开始时间的数字。 通常,在工作机器上,相似的数字较为适中,但由于我们对绝对值不感兴趣,而对百分比增量感兴趣,因此我们认为这种差异不明显。
- 为了避免过早进入测量共享内存的复杂性,现在我们将省略以字节为单位的准确读数。 取而代之的是,我们引入“ CDS潜力 ”的概念,表示为共享类数相对于已加载类总数的百分比。 当然,这是一个抽象的数量,但另一方面,它直接影响实际的内存消耗。 此外,其定义完全不依赖于操作系统,并且对于其计算,仅日志就足够了。
参考点
让这一点成为新下载的应用程序的状态,即 无需明确使用任何AppCDS'ov等。 要对其进行评估,我们需要:
安装OpenJDK 13(例如,国内Liberica发行版,但不是lite版本)。
例如,还需要将其添加到PATH环境变量或JAVA_HOME
,如下所示:
export JAVA_HOME=~/tools/jdk-13
下载 ANALOG(在撰写本文时,最新版本为v0.12.1)。
如有必要,您可以在server.address
参数的config/application.yaml
文件中指定用于访问该应用程序的外部主机名(默认情况下,在其中指定localhost
)。
启用JVM类负载日志记录。
为此,您可以使用以下值来启动JAVA_OPTS
环境变量:
export JAVA_OPTS=-Xlog:class+load=info:file=log/class-load.log
该选项将传递给JVM,并告诉它保证每个类的源。
运行测试运行:
- 使用
bin/analog
脚本运行应用程序 - 在浏览器中打开http://本地主机:8083 ,戳按钮和daws
- 通过在
bin/analog
脚本控制台中按Ctrl+C
来停止应用程序
获取结果(来自log/
目录中的文件)
加载的类总数(通过class-load.log
):
cat class-load.log | wc -l 10463
从共享归档文件中下载了多少个(根据它):
grep -o 'source: shared' - class-load.log 1146
平均启动时间(一系列启动后;通过analog.log
):
grep -oE '\(JVM running for .+\)' analog.log | grep -oE '[0-9]\.[0-9]+' | awk '{ total += $1; count++ } END { print total/count }' 4.5225
因此,在此步骤中,CDS的潜力为1146/10463=0,1095
0.1095≈11% 。 如果您对共享类的来源感到惊讶(毕竟,我们还没有包含任何AppCDS),那么我想提醒您,从第12版开始,JDK 包括完成的CDS归档文件$JAVA_HOME/lib/server/classes.jsa
,已构建不少于准备好的班级清单:
cat $JAVA_HOME/lib/classlist | wc -l 1170
现在,在评估了应用程序的初始状态之后,我们可以将AppCDS应用于该应用程序,并通过比较来了解它的作用。
核心经验
正如文档所遗留的那样,要创建一个动态AppCDS存档,您只需使用-XX:ArchiveClassesAtExit
选项对应用程序进行一次试运行-XX:ArchiveClassesAtExit
。 从下一次启动以来,就可以使用存档并从中获利。 要在同一只实验兔子(AnaLog)上进行验证,您需要:
将指定的选项添加到运行命令:
export JAVA_OPTS="$JAVA_OPTS -XX:ArchiveClassesAtExit=work/classes.jsa"
扩展日志记录:
export JAVA_OPTS="$JAVA_OPTS -Xlog:cds=debug:file=log/cds.log"
此选项将强制在应用程序停止时记录构建CDS存档的过程。
执行与参考点相同的测试运行:
- 使用
bin/analog
脚本运行应用程序 - 在浏览器中打开http://本地主机:8083 ,戳按钮和daws
- 通过在
bin/analog
脚本控制台中按Ctrl+C
来停止应用程序
在那之后,带有各种警告的巨大脚步应该落入控制台,并且log/cds.log
应该填充详细信息; 他们对我们还不感兴趣。
将启动模式从试用模式切换为有用模式:
export JAVA_OPTS="-XX:SharedArchiveFile=work/classes.jsa -Xlog:class+load=info:file=log/class-load.log -Xlog:class+path=debug:file=log/class-path.log"
在这里,我们不补充JAVA_OPTS
变量,而是用新值重新擦除它,这些新值包括(1)使用共享档案库,(2)记录类源和(3)记录类路径检查。
根据第3段中的方案对应用程序进行有用的启动。
获取结果(来自log/
目录中的文件)
验证AppCDS是否确实适用(通过class-path.log
):
[0.011s][info][class,path] type=BOOT [0.011s][info][class,path] Expecting BOOT path=/home/upc/tools/jdk-13/lib/modules [0.011s][info][class,path] ok [0.011s][info][class,path] type=APP [0.011s][info][class,path] Expecting -Djava.class.path=/home/upc/tmp/analog/lib/analog.jar [0.011s][info][class,path] ok
type=BOOT
和type=APP
行之后的ok
标记分别表示成功打开,验证和加载了内置CDS存档和应用CDS存档。
加载的类总数(通过class-load.log
):
cat class-load.log | wc -l 10403
从共享归档文件中下载了多少个(根据它):
grep -o 'source: shared' -c class-load.log 6910
平均启动时间(一系列启动后;通过analog.log
文件):
grep -oE '\(JVM running for .+\)' analog.log | grep -oE '[0-9]\.[0-9]+' | awk '{ total += $1; count++ } END { print total/count }' 4.04167
但是在此步骤中,CDS的潜力已经为6910/10403≈0,66
66% ,即与参考点相比增加了55% 。 同时,平均发射时间减少了(4,5225-4,04167)=0,48
秒,即 启动速度快了初始值的10.6% 。
结果分析
该项目的工作标题是:“为什么这么少?”
就像我们一样,我们按照说明进行了所有操作,但是并不是所有的类都在存档中。 它们的数量对发射时间的影响不小于实验者机器的计算能力,因此我们将集中在这个数字上。
如果您还记得的话,我们会在试运行后忽略实验应用程序停止期间创建的log/cds.log
文件。 在此HotSpot文件中,JVM友好地指出了未出现在CDS归档文件中的每个警告类。 这是此类标记的总数:
grep -o '[warning]' cds.log -c 3591
考虑到class-load.log
日志中仅提及10K +类,并且其中66%的类是从存档中加载的,因此不难理解cds.log
中列出的3600个类是CDS潜力的“缺失” 44%。 现在,您需要找出为什么跳过它们。
如果查看cds.log日志,结果发现跳过类只有四个独特的原因。 以下是每个示例的示例:
Skipping org/springframework/web/client/HttpClientErrorException: Not linked Pre JDK 6 class not supported by CDS: 49.0 org/jrobin/core/RrdUpdater Skipping java/util/stream/Collectors$$Lambda$554: Unsafe anonymous class Skipping ch/qos/logback/classic/LoggerContext: interface org/slf4j/ILoggerFactory is excluded
在所有3591个错过的课程中,这些原因的出现频率如下:

仔细看看它们:
Unsafe anonymous class
JVM “” , -, .
Not linked
, “” , , . , StackOverflow . , , “” () JAR- , AppCDS. , ( ).
Pre JDK 6 class
, CDS Java 5. class- , CDS . , , 6, Java, . - , runtime- (, slf4j).
Skipping ... : super class/interface ... is excluded
, “” . CDS', . 例如:
[warning][cds] Pre JDK 6 class not supported by CDS: 49.0 org/slf4j/spi/MDCAdapter [warning][cds] Skipping ch/qos/logback/classic/util/LogbackMDCAdapter: interface org/slf4j/spi/MDCAdapter is excluded
结论
CDS 100%.
, , , , , . .
JEP-310 , AppCDS JDK. . , . CDS (, , ) .
( ), - ; “ ”. Spring Boot, - ; JVM. ANALOG_OPTS
, Gradle'.
export ANALOG_OPTS="-Djavamelody.enabled=false -Dlogging.config=classpath:logging/logback-console.xml" export ANALOG_OPTS="$ANALOG_OPTS -Dnodes.this.agentPort=7801 -Dserver.port=8091"
JavaMelody, , , . TCP- ; .
, , JVM AppCDS . JAVA_OPTS
JVM Unified Logging Framework :
export JAVA_OPTS="-Xlog:class+load=info:file=log/class-load-%p.log -Xlog:class+path=debug:file=log/class-path-%p.log" export JAVA_OPTS="$JAVA_OPTS -XX:SharedArchiveFile=work/classes.jsa"
%p
, JVM (PID). AppCDS , ( ).
, . . :
server.port
nodes.this.agentPort
, :
export ANALOG_OPTS="$ANALOG_OPTS -Dnodes.this.agentPort=7801 -Dserver.port=8091"
, ( ).
bin/analog
() http://localhost:8091 ,
PID ( ), :
pgrep -f analog 13792
pmap
( ):
pmap -XX 13792 | sed -n -e '2p;$p' Address Perm Offset Device Inode Size KernelPageSize MMUPageSize Rss Pss Shared_Clean Shared_Dirty Private_Clean Private_Dirty Referenced Anonymous LazyFree AnonHugePages ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked ProtectionKey VmFlagsMapping 3186952 1548 1548 328132 325183 3256 0 10848 314028 212620 314024 0 0 0 0 0 0 0 325183 0 KB
; .
1-4 (, ).
pmap
. CDS' . , , PSS:
The "proportional set size" (PSS) of a process is the count of pages it has in memory, where each page is divided by the number of processes sharing it. So if a process has 1000 pages all to itself, and 1000 shared with one other process, its PSS will be 1500.
, , “ ” . , .
PSS , :
, - :
, . AppCDS. , -XX:SharedArchiveFile=work/classes.jsa
-Xshare:off
, CDS . , .

:
PSS AppCDS CDS.
. , , HelloWorld- JVM CDS 2 , CDS. PSS CDS, . :
PSS AppCDS 2- ; 3- .
, , , . , AppCDS, , , 3- .
: , CDS? :
CDS/AppCDS JVM , PSS . , , pmap
, “” sed
'. :
pmap -X `pgrep -f analog` 14981:
( Mapping
) , “” . JVM ( libjvm.so
), ( libc-2.27.so
). :
For the Java VM, the read-only parts of the loaded shared libraries (ie libjvm.so
) can be shared between all the VM instances running at the same time. This explains why, taking together, the two VM's consume less memory (ie have a smaller memory footprint) than the simple sum of their single resident set sizes when running alone.
. , , . , , JVM , Java- . GeekOut:

, , , AppCDS , .. Java-. , JVM, , - .
VisualVM Metaspace AppCDS , :
AppCDS

AppCDS

, 128 Metaspace AppCDS 64.2 MiB / 8.96 MiB
≈7,2 , CDS . (. ) 66.4 MiB / 13.9 MiB
≈4,8 . , AppCDS , Metaspace. Metaspace, , CDS .
而不是结论
Spring Boot AppCDS – JVM, .
- JEP-350 Dynamic CDS Archives – JDK 13.
- Spring Boot ó CDS ( ). , 100% - 66% . , ≈11% ( 15%, ).
- , 5- PSS ( ). , AppCDS , , 8% (PSS). , CDS, , . AppCDS .
- Metaspace, , AppCDS 5 , CDS.
, , AppCDS, , “killer feature”. Spring Boot. , , AppCDS . , , AppCDS Spring Boot. , …
by Nick Fewings on Unsplash