本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
Java 作为一个运行在虚拟机上的编译解释型应用,一直因为其启动速度问题而饱受诟病,这么多年以来,Java 推出了三个主要手段来解决启动慢的问题。
CDS、CRaC 和 GraalVM,这三种技术都有助于提高Java程序的启动速度,但它们的应用场景和优化方式有所不同。
CDS 通过存档类数据来加速启动,CRaC 通过检查点快速恢复进行快速启动,而 GraalVM 则通过 AOT 编译来实现飞速行动。
今天我就给大家介绍一下 Java 官方最早推出的类加速手段:CDS。
Java 的 CDS (Class Data Sharing) 是一种可以将应用程序的类元数据和一些常量数据预先加载到共享内存中,并在多个 JVM 实例之间共享的技术,通过这样做可以提高 Java 应用程序的启动速度和内存使用效率。
CDS 的优势:
- 提高启动速度: 通过预先加载类元数据,可以减少 JVM 启动时加载类的开销,从而提高应用程序的启动速度。
- 节省内存: 多个 JVM 实例可以共享相同的类元数据,从而减少内存占用。
- 提高吞吐量: 通过减少类加载的开销,可以提高应用程序的吞吐量。
除此之外,Java 在升级的过程中也没有忘记这个特性,在多个版本中一直迭代升级,以求达到更好的效果。
JDK5 中的 CDS
一切还要从 JDK5 开始说起,在 JDK5 中,CDS 被第一次引进,这个时候 Java 的开发者们就已经在想办法处理 Java 启动慢的问题。
JDK5 中的解决办法是将 Java 的常用类(lang 包下的类)做一个类的元数据存档,存档出来的类可以被多个 JVM 实例共享,这意味着当你启动 JVM 实例的时候它们会自动寻找存档好的元数据进行加载,不必再经过加载、验证、解析、初始化这些步骤了。
接下来我会举一个小小的例子,在你的机器上运行:java -Xshare:dump
命令,会得到这样一个输出:
ubuntu@ip-172-26-0-162:~$ sudo java -Xshare:dump
Allocated shared space: 37871616 bytes at 0x0000000800000000
Loading classes to share ...
Preload Warning: Cannot find sun/misc/PostVMInitHook
Preload Warning: Cannot find sun/usagetracker/UsageTrackerClient
Preload Warning: Cannot find sun/usagetracker/UsageTrackerClient$1
Preload Warning: Cannot find sun/usagetracker/UsageTrackerClient$4
Preload Warning: Cannot find sun/usagetracker/UsageTrackerClient$3
Preload Warning: Cannot find sun/dc/DuctusRenderingEngine
Preload Warning: Cannot find java/net/InetAddress$Cache
Preload Warning: Cannot find java/net/InetAddress$Cache$Type
Preload Warning: Cannot find java/net/InetAddress$CacheEntry
Preload Warning: Cannot find sun/font/T2KFontScaler
Preload Warning: Cannot find sun/font/T2KFontScaler$1
Preload Warning: Cannot find javax/xml/parsers/SecuritySupport$5
Preload Warning: Cannot find sun/security/provider/DSA$LegacyDSA
Loading classes to share: done.
Rewriting and linking classes ...
Rewriting and linking classes: done
Number of classes 2562
instance classes = 2548
obj array classes = 6
type array classes = 8
Calculating fingerprints ... done.
Removing unshareable information ... done.
ro space: 7362152 [ 36.3% of total] out of 16777216 bytes [43.9% used] at 0x00000008000
00000
rw space: 11270608 [ 55.6% of total] out of 16777216 bytes [67.2% used] at 0x00000008010
00000
md space: 1589064 [ 7.8% of total] out of 4194304 bytes [37.9% used] at 0x00000008020
00000
mc space: 34053 [ 0.2% of total] out of 122880 bytes [27.7% used] at 0x00000008024
00000
total : 20255877 [100.0% of total] out of 37871616 bytes [53.5% used]
执行这个命令之后,Java 就会在你的机器上生成一个元数据的存档文件,默认 CDS 档案位于以下位置:
- 在 Linux 和 macOS 平台上,共享存档存储在
/lib/[arch]/server/classes.jsa
- 在 Windows 平台上,共享档案存储在
/bin/server/classes.jsa
然后你就可以在启动 Java 程序的时候带上 -Xshare:on
参数,你的 JVM 程序就会自动寻找这个文件进行加载了。
⚠️:如果你带上这个参数却没有这个文件则会你的 JVM 程序则会启动失败。
如果你不需要进行这种方式的加载,可以使用 -Xshare:off
参数关闭它,如果你希望更加灵活一点,只在此文件存在的时候加载,没有的时候则不加载,可以带上 -Xshare:auto
参数,它会自动判断。
JDK8、9、10 中的 CDS
也许你已经注意到了,这个存档文件只会存档 JDK 的一些必要类,并不会对我们的自己写的类代码进行存档加速,所以在 Oracle中的 JDK8 中其实提供了一个商业特性用于解决这个问题——AppCDS。
AppCDS 允许你在类存档的时候添加一个类列表,JDK 工具会将这个类列表中的类也一并进行存档。
首先,你应该这样启动你的程序:
java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst -cp hello.jar HelloWorld
AppCDS 这个特性中引入了一个新的参数:-XX:+UseAppCDS
,在启动参数中加上它代表你将打开 AppCDS 功能。
接着我们使用 -XX:DumpLoadedClassLis 参数,这个参数会记录你的应用会加载哪些类,他们会被记录到一个名为 hello 的 lst 文件中去。
然后, 你需要开始创建 AppCDS 存档,使用以下命令:
java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \
-XX:SharedArchiveFile=hello.jsa -cp hello.jar
我们在启动参数中带上了 -Xshare:dump 和 -Xshare:dump,它们代表使用 AppCDS 的方式进行存档。
接着使用 -XX:SharedClassListFile 来表示类文件目录,它对应着上面提到过的 -XX:DumpLoadedClassLis 参数。
接着是使用 -XX:SharedArchiveFile 来指定存档的文件存储在哪个位置,以及它的文件名是什么,如果你不指定这个参数,它会被默认存储在你的 JDK 安装目录。
最后一步,使用这个存档后的文件,这代表着你需要这样启动应用:
java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
-cp hello.jar HelloWorld
这个命令主要打开 AppCDS 之后,使用 -XX:SharedArchiveFile 指定我们之前存储的 jsa 存档文件。
以上,就是 AppCDS 加速的整个使用过程,虽然有一些繁琐,但是 CDS 这个功能也确实有了长足的进步。
在 Oracle 中的 JDK8 和 JDK9 中都具有 AppCDS 特性,终于在 OpenJDK 10 的时候,OpenJDK 将 AppCDS 作为一个正式特性加入了 OpenJDK,此功能的提案是 JEP310。
JDK12、JDK13 中的 CDS
在 Oracle JDK12 中,CDS 变成了一个默认开启的功能,JDK 在编译打包的时候就会使用我们前面提到过的 -Xshare:dump
参数创建一个存档。
所以从 JDK12 开始,CDS 变成了一个必须使用 -Xshare:off 参数才能禁用的功能。
考虑到 Oracle JDK 已经把大部分的独有代码都贡献给了 OpenJDK,所以 OpenJDK 12 也应该支持了此功能,感兴趣的可以去深挖一下。
前文中,我们提到了 AppCDS,虽然这个功能效果非常好,但是对我们使用者来说你需要完成三个步骤才能使用上 AppCDS,繁琐复杂容易出错。
所以在 JDK13 中,OpenJDK 官方优化了这个功能,JDK 13 中的这个提案被称作 JEP350,感兴趣的同学可以点击这里查看。
优化后的 AppCDS 官方称之为动态 AppCDS,因为它可以在应用被关闭后自动归档,这样存档后生成的文件一般来说就具有了你整个应用程序所需要的所有的类。
当你想使用这个特性时,你需要用到以下命令:
java -XX:ArchiveClassesAtExit=myapp.jsa -jar MyComplexApp.jar
-XX:ArchiveClassesAtExit
这个参数代表着会在程序结束后自动存档。
当时想使用这个存档文件时,你需要用到以下命令:
java -XX:SharedArchiveFile=myapp.jsa -jar MyComplexApp.jar
-XX:SharedArchiveFile
参数就代表了上文中你指定的存档路径。
在此多说一句,使用动态 AppCDS 有一个前提,就是你需要已经有了核心类的 CDS 存档,即 JDK 本身的核心类存档,这应该也是 JDK12 默认进行 CDS 存档的原因。
展望~
经过这么多版本的迭代,CDS 已经有了长足的进步,但是似乎也无法再进一步了。
所以在最近的 JDK 版本中推出了 CRaC 和 GraalVM,这两个都代表了未来 Java 减少启动时间的主要手段,这两个特性虽然并未成为标准主线特性,但是已经有很多用户用上了,大家可以继续期待一下。
感谢大家能看到这,同时也希望大家能对本篇点点赞,让更多的人看到优质的内容,点赞过 100 一周内更新更多高级 Java 知识,有任何问题都可以在评论区一块讨论,祝有好收获。