JEP 295:提前编译
概括
在启动虚拟机之前将 Java 类编译为本机代码。
目标
-
改善小型和大型 Java 应用程序的启动时间,对峰值性能的影响至多有限。
-
尽可能少地改变最终用户的工作流程。
非目标
没有必要提供显式的、公开的类似库的机制来保存和加载编译的代码。
动机
JIT 编译器速度很快,但 Java 程序可能会变得非常大,以至于 JIT 需要很长时间才能完全预热。不常用的 Java 方法可能根本不会被编译,可能会因重复解释调用而导致性能损失。
描述
任何 JDK 模块、类或用户代码的 AOT 编译都是实验性的,JDK 9 不支持。
要使用 AOTedjava.base
模块,用户必须编译该模块并将生成的 AOT 库复制到 JDK 安装目录中,或在 java 命令行上指定它。 AOT 编译代码的使用对于最终用户来说是完全透明的。
AOT 编译是由一个新工具完成的jaotc
:
jaotc --output libHelloWorld.so HelloWorld.class
jaotc --output libjava.base.so --module java.base
它使用Graal作为代码生成后端。
在 JVM 启动期间,AOT 初始化代码在众所周知的位置查找众所周知的共享库,或者在命令行上使用 AOTLibrary 标志指定。如果找到共享库,则会拾取并使用它们。如果未找到共享库,则该 JVM 实例的 AOT 将被关闭。
java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld
以下小节列出了新的 java AOT 标志和jaotc
标志,其中还包含有关如何为java.base
模块构建和安装 AOT 库的说明。
AOT 编译代码使用的容器格式是共享库。 JDK 9版本仅支持Linux/x64,其中共享库格式为ELF。 AOT 库中 AOT 编译的代码被 JVM 视为现有 CodeCache 的扩展。当加载 java 类时,JVM 会查找加载的 AOT 库中是否存在相应的 AOT 编译方法,并从 java 方法描述符添加指向它们的链接。 AOT 编译的代码遵循与普通 JIT 编译的代码相同的调用/去优化/卸载规则。
由于类字节码可能会随着时间的推移而发生变化,无论是通过更改源代码还是通过类转换和重新定义,JVM 需要检测此类更改,并在字节码不匹配时拒绝 AOT 编译的代码。这是通过_类指纹识别_来实现的。在 AOT 编译期间,会生成每个类的指纹并将其存储在共享库的数据部分中。稍后,当加载一个类并找到该类的 AOT 编译代码时,会将当前字节码的指纹与存储在共享库中的指纹进行比较。如果存在不匹配,则不会使用该特定类的 AOT 代码。
AOT 编译和执行期间应使用相同的 JDK。 Java 版本记录在 AOT 库中并在加载期间进行检 查。 Java更新时需要AOT重新编译。
jaotc
不解析不是系统类或编译类的一部分的引用类。它们必须添加到类路径中。否则,在 AOT 编译期间可能会抛出 ClassNotFoundException。
jaotc --output=libfoo.so --jar foo.jar -J-cp -J./
jaotc --output=libactivation.so --module java.activation -J--add-module=java.se.ee
AOT使用
使用jaotc
工具执行AOT编译。该工具是 java 安装的一部分 - 与javac
.
jaotc --output libHelloWorld.so HelloWorld.class
然后在应用程序执行期间指定生成的AOT库:
java -XX:AOTLibrary=./libHelloWorld.so HelloWorld
对于此版本,在 AOT 编译和执行期间应使用相同的 java 运行时配置。例如:
jaotc -J-XX:+UseParallelGC -J-XX:-UseCompressedOops --output libHelloWorld.so HelloWorld.class
java -XX:+UseParallelGC -XX:-UseCompressedOops -XX:AOTLibrary=./libHelloWorld.so HelloWorld
它包括使用相同 JDK 构建变体的要求:产品或调试。
运行时配置记录在 AOT 库中,并在执行期间加载库时进行验证。如果验证失败,则不会使用此 AOT 库,并且 JVM 将继续运行或退出(如果-XX:+UseAOTStrictLoading
指定了标志)。
在 JVM 启动期间,AOT 初始化代码会在众所周知的位置查找众所周知的共享库或由-XX:AOTLibrary
选项指定的库。如果找到共享库,则会拾取并使用它们。如果找不到共享库,则此 JVM 实例运行的 AOT 将被关闭。
AOT 库可以通过标志控制以两种模式编译--compile-for-tiered
:
- _非分层 AOT_编译代码的行为与静态编译的 C++ 代码类似 ,因为不会收集分析信息,也不会发生 JIT 重新编译。
- _分层 AOT_编译代码确实会收集分析信息。完成的分析与在第 2 层编译的 C1 方法完成的_简单分析_相同。如果 AOT 方法达到 AOT 调用阈值,则这些方法首先由第 3 层的 C1 重新编译,以便收集完整的分析信息。这是 C2 JIT 重新编译所必需的,以便生成最佳代码并达到峰值应用程序性能。
在第 3 层重新编译代码的额外步骤是必要的,因为完整分析的开销太高,无法用于所有方法,特别是对于诸如java.base
.对于用户应用程序来说,允许使用第 3 层等效分析进行 AOT 编译可能是有意义的,但 JDK 9 不支持这一点。
逻辑编译模式java.base
是分层 AOT,因为java.base
需要对方法进行 JIT 重新编译以达到峰值性能。仅在某些情况下,非分层 AOT 编译才有意义。这包括需要可预测行为的应用程序(当占用空间比峰值性能更重要时),或者对于不允许动态代码生成的系统。在这些情况下,AOT 编译需要在整个应用程序上完成,因此在 JDK 9 中是实验性的。
可以为不同的执行环境生成AOT库集。 JVM 知道为特定运行时配置生成的 AOT 库well-known
的下一个名称。java.base
它将在 $JAVA_HOME/lib 目录中查找它们并加载与当前运行时配置相对应的:
-XX:-UseCompressedOops -XX:+UseG1GC : libjava.base.so
-XX:+UseCompressedOops -XX:+UseG1GC : libjava.base-coop.so
-XX:-UseCompressedOops -XX:+UseParallelGC : libjava.base-nong1.so
-XX:+UseCompressedOops -XX:+UseParallelGC : libjava.base-coop-nong1.so
JVM 还知道下一个 java 模块的 AOT 库名称,但它们的编译、安装和使用都是实验性的:
java.base
jdk.compiler
jdk.scripting.nashorn
jdk.internal.vm.ci
jdk.internal.vm.compiler
java.base
为模块生成和使用 AOT 库的步骤
使用编译java.base
模块jaotc
。它需要很大的java堆来保存所有编译方法的数据(大约50000个方法):
jaotc -J-XX:+UseCompressedOops -J-XX:+UseG1GC -J-Xmx4g --compile-for-tiered --info --compile-commands java.base-list.txt --output libjava.base-coop.so --module java.base
一些方法会java.base
导致编译失败,可以通过使用--compile-comands
选项排除:
cat java.base-list.txt
# jaotc: java.lang.StackOverflowError
exclude sun.util.resources.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.TimeZoneNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.cldr.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
# java.lang.Error: Trampoline must not be defined by the bootstrap classloader
exclude sun.reflect.misc.Trampoline.<clinit>()V
exclude sun.reflect.misc.Trampoline.invoke(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
# JVM asserts
exclude com.sun.crypto.provider.AESWrapCipher.engineUnwrap([BLjava/lang/String;I)Ljava/security/Key;
exclude sun.security.ssl.*
exclude sun.net.RegisteredDomain.<clinit>()V
# Huge methods
exclude jdk.internal.module.SystemModules.descriptors()[Ljava/lang/module/ModuleDescriptor;
生成 AOT 库后,在应用程序执行期间使用-XX:AOTLibrary
选项指定它(默认情况下,java 在 JDK 9 中使用 G1 和压缩 oops - 您不需要指定这些标志):
java -XX:AOTLibrary=./libjava.base-coop.so,./libHelloWorld.so HelloWorld
或者将生成的AOT库复制到JDK安装目录(您可能需要调整目录的权限):
cp libjava.base-coop.so $JAVA_HOME/lib/
在这种情况下,它将自动加载,无需在命令行上指定:
java -XX:AOTLibrary=./libHelloWorld.so HelloWorld
考虑strip
使用 AOT 库中未使用的符号来减少库的大小。
新的运行时 AOT 标志
-XX:+/-UseAOT
使用 AOT 编译的文件。默认情况下它是打开的。
-XX:AOTLibrary=<file>
指定 AOT 库文件的列表。使用冒号 (:) 或逗号 (,) 分隔库条目。
-XX:+/-PrintAOT
打印使用的 AOT 类和方法。
附加诊断标志可用(需要指定 -XX:+UnlockDiagnosticVMOptions 标志):
-XX:+/-UseAOTStrictLoading
如果任何 AOT 库的运行时配置与当前运行时设置不匹配,则退出 JVM。
JVM 运行时具有以下与JEP 158 集成的统一日志记录 AOT 标签: 统一 JVM 日志记录。
aotclassfingerprint
创建日志案例类的指纹与 AOT 库中记录的指纹不匹配。
aotclassload
当AOT库中找到相应的类数据时创建日志。
aotclassresolve
当 AOT 编译代码解析类的请求成功或失败时创建日志。
jaotc:Java 提前编译器
jaotc
是java静态编译器,它为编译后的java方法生成本机代码。它使用 Graal 作为代码生成后端,并使用 libelf 来生成 .so AOT 库。
该工具是 java 安装的一部分,使用方式与javac
.
jaotc <options> <name or list>
其中“name”是类名或 jar 文件。 'list' 是一个 : 分隔的类名、模块、jar 文件或包含类文件的目录的列表。
jaotc
可以使用以下选项:
--output <file>
输出文件名。默认名称是“unnamed.so”。
--class-name <class names>
要编译的 Java 类列表
--jar <jar files>
要编译的 jar 文件列表
--module <modules>
要编译的 Java 模块列表
--directory <dirs>
搜索要编译的文件的目录列表
--search-path <dirs>
搜索指定文件的目录列表
--compile-commands <file>
带有编译命令的文件名:
exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.security.ssl.*
compileOnly java.lang.String.*
AOT 目前识别两个编译命令:
exclude - exclude compilation of specified methods
compileOnly - compile only specified methods
正则表达式用于指定类和方法。
--compile-for-tiered
为分层编译生成分析代码。默认情况下,不会生成分析代码(将来可能会更改)。
--compile-with-assertions
使用 java 断言生成代码。默认情况下不生成断言代码。
--compile-threads <number>
要使用的编译线程数。默认值为min(16, available_cpus)。
--ignore-errors
忽略类加载期间引发的所有异常。如果类加载抛出异常,默认退出编译。
--exit-on-error
出现编译错误时退出。默认情况下,会跳过失败的编译并继续编译其他方法。
--info
打印有关编译阶段的信息
--verbose
打印有关编译阶段的更多详细信息,打开 --info 标志
--debug
打印更多详细信息,打开 --info 和 --verbose 标志
--help
打印jaotc
使用消息和标志
--version
打印版本信息
-J<flag>
将标志直接传递给 JVM 运行时系统
当前 AOT 限制
- JDK 9 中的 AOT 初始版本仅供实验使用,并且仅限于运行带有并行或 G1 GC 的 64 位 Java 的 Linux x64 系统。
- AOT 编译必须在 Java 应用程序使用 AOT 代码的同一系统或具有相同配置的系统上执行。
- 在 AOT 编译和执行期间必须使用相同的 Java 运行时配置。例如,如果应用程序还 将并行 GC 与 AOT 代码一起使用,则该
jaotc
工具应与并行 GC 一起运行(使用标志)。-J
不匹配的运行时配置可能会导致应用程序在执行期间崩溃。 - 可能无法编译使用动态生成的类和字节码(lambda 表达式、动态调用)的 Java 代码。任何未经 AOT 编译的代码都将在运行时执行期间以通常的方式处理:首先在解释器中运行,然后由 JIT 编译器编译。
- AOT 不支持自定义类加载器,因为在 AOT 编译期间它没有有关它们的信息。 AOT 编译的方法不会用于非内置加载器加载的类。
这些限制可能会在未来的版本中得到解决。
备择方案
已经讨论了配置文件或编译决策的保存,但这并不能减少实际编译代码所花费的时间。有可能可以改为保存低级 IR 的最新副本,但这似乎并不那么复杂。
测试
将开发新的 AOT jtreg 测试来测试 AOT 功能。
所有现有测试都可以使用支持 AOT 的 JDK 运行。这已经作为单独的夜间测试配置完成。
另一种配置在带有 AOT 编译java.base
模块的支持 AOT 的 JDK 上运行所有测试。
风险和假设
使用预编译代码可能会导致使用不太理想的代码,从而导致性能损失。性能测试表明,一些应用程序受益于 AOT 编译的代码,而其他应 用程序则明显表现出回归。由于 AOT 功能是一项可选功能,因此用户应用程序可能出现的性能下降是可以避免的。如果用户发现应用程序启动速度更慢,或者没有达到预期的峰值性能,或者崩溃,他们可以使用该-XX:-UseAOT
标志关闭 AOT,或者删除任何 AOT 库。
建议 AOT 编译在受信任的环境中进行,其中 JDK 库和工具受到保护以防止篡改。
依赖关系
该项目依赖于JEP 243: Java-Level JVM Compiler Interface,因为 AOT 编译器使用 Graal 作为代码生成后端,而后者又依赖于 JVMCI。
该项目将把Graal 核心合并到 JDK 中,并在 Linux/x64 版本中提供它。