Java Ahead of Time (AOT) 编译器
1. 简介
在本文中,我们将介绍 Java Ahead of Time (AOT) 编译器,该编译器在JEP-295 中进行了描述,并在 Java 9 中作为实验性特性添加。
首先,我们将了解 AOT 是什么,其次,我们将看一个简单的示例。第三,我们将看到 AOT 的一些限制,最后,我们将讨论一些可能的用例。
2. 什么是提前编译?
AOT 编译是提高 Java 程序性能,尤其是 JVM 启动时间的一种方法。JVM 执行 Java 字节码并将经常执行的代码编译为本机代码。这称为即时 (JIT) 编译。JVM 根据执行期间收集的分析信息决定要 JIT 编译哪些代码。
虽然这种技术使 JVM 能够生成高度优化的代码并提高峰值性能,但启动时间可能不是最佳的,因为执行的代码尚未 JIT 编译。AOT 旨在改善这个所谓的预热期。用于 AOT 的编译器是 Graal。
**在本文中,我们不会详细介绍 JIT 和 Graal。**请参阅我们的其他文章,了解Java 9 和 10 的性能改进概述 ,以及 对 Graal JIT 编译器 的深入了解。
3. 例子
对于这个例子,我们将使用一个非常简单的类,编译它,然后看看如何使用生成的库。
3.1. AOT 编译
让我们快速浏览一下我们的示例类:
public class JaotCompilation {
public static void main(String[] argv) {
System.out.println(message());
}
public static String message() {
return "The JAOT compiler says 'Hello'";
}
}
在我们可以使用 AOT 编译器之前,我们需要使用 Java 编译器编译该类:
javac JaotCompilation.java
然后我们将生成的 JaotCompilation.class传递 给 AOT 编译器,它与标准 Java 编译器位于同一目录中:
jaotc --output jaotCompilation.so JaotCompilation.class
这会在当前目录中生成库 jaotCompilation.so 。
3.2. 运行程序
然后我们可以执行程序:
java -XX:AOTLibrary=./jaotCompilation.so JaotCompilation
参数*-XX:AOTLibrary* 接受库的相对或完整路径。或者,我们可以将库复制到 Java 主目录中的lib文件夹中,并且只传递库的名称。
3.3. 验证库是否被调用和使用
通过添加 -XX:+PrintAOT 作为 JVM 参数,我们可以看到该库确实已加载:
java -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so JaotCompilation
输出将如下所示:
77 1 loaded ./jaotCompilation.so aot library
但是,这仅告诉我们库已加载,而不是实际使用。通过传递参数*-verbose*,我们可以看到库中的方法确实被调用了:
java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation
输出将包含以下行:
11 1 loaded ./jaotCompilation.so aot library
116 1 aot[ 1] jaotc.JaotCompilation.<init>()V
116 2 aot[ 1] jaotc.JaotCompilation.message()Ljava/lang/String;
116 3 aot[ 1] jaotc.JaotCompilation.main([Ljava/lang/String;)V
The JAOT compiler says 'Hello'
AOT 编译库包含一个类指纹,它必须与.class*文件的指纹匹配。*
让我们更改JaotCompilation.java类中的代码以返回不同的消息:
public static String message() {
return "The JAOT compiler says 'Good morning'";
}
如果我们在没有 AOT 编译修改后的类的情况下执行程序:
java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation
然后输出将仅包含:
11 1 loaded ./jaotCompilation.so aot library
The JAOT compiler says 'Good morning'
**我们可以看到库中的方法不会被调用,因为类的字节码已经改变。**这背后的想法是,无论是否加载了 AOT 编译库,程序总是会产生相同的结果。
4. 更多 AOT 和 JVM 参数
4.1. Java 模块的 AOT 编译
也可以 AOT 编译模块:
jaotc --output javaBase.so --module java.base
生成的库javaBase.so大小约为 320 MB,加载需要一些时间。可以通过选择要 AOT 编译的包和类来减小大小。
我们将在下面介绍如何做到这一点,但是,我们不会深入研究所有细节。
4.2. 使用编译命令进行选择性编译
**为了防止 Java 模块的 AOT 编译库变得太大,我们可以添加编译命令来限制 AOT 编译的范围。**这些命令需要在一个文本文件中——在我们的示例中,我们将使用文件 complileCommands.txt:
compileOnly java.lang.*
然后,我们将其添加到编译命令中:
jaotc --output javaBaseLang.so --module java.base --compile-commands compileCommands.txt
生成的库将仅包含java.lang 包中的 AOT 编译类。
为了获得真正的性能改进,我们需要找出在 JVM 预热期间调用了哪些类。
这可以通过添加几个 JVM 参数来实现:
java -XX:+UnlockDiagnosticVMOptions -XX:+LogTouchedMethods -XX:+PrintTouchedMethodsAtExit JaotCompilation
在本文中,我们不会深入研究这种技术。
4.3. 单个类的 AOT 编译
我们可以使用参数 –class-name 编译单个类:
jaotc --output javaBaseString.so --class-name java.lang.String
生成的库将仅包含类String。
4.4. 为分层编译
默认情况下,将始终使用 AOT 编译的代码,并且库中包含的类不会发生 JIT 编译。如果我们想在库中包含分析信息,我们可以添加参数 compile-for-tiered:
jaotc --output jaotCompilation.so --compile-for-tiered JaotCompilation.class
库中的预编译代码将一直使用,直到字节码符合 JIT 编译条件。
5. AOT 编译的可能用例
AOT 的一个用例是短运行程序,它在任何 JIT 编译发生之前完成执行。
另一个用例是嵌入式环境,其中 JIT 是不可能的。
此时,我们还需要注意的是,AOT编译库只能从具有相同字节码的Java类中加载,因此无法通过JNI加载。
6. AOT 和 AWS Lambda
AOT 编译代码的一个可能用例是短寿命的 lambda 函数,其中短启动时间很重要。在本节中,我们将了解如何在 AWS Lambda 上运行 AOT 编译的 Java 代码。
**使用 AWS Lambda 进行 AOT 编译需要在与 AWS 上使用的操作系统兼容的操作系统上构建库。**在撰写本文时,这是Amazon Linux 2。
此外,Java 版本需要匹配。AWS 提供了Amazon Corretto Java 11 JVM。为了有一个环境来编译我们的库,我们将在 Docker 中安装Amazon Linux 2和Amazon Corretto 。
我们不会讨论使用 Docker 和 AWS Lambda 的所有细节,而只会概述最重要的步骤。有关如何使用 Docker 的更多信息,请参阅此处 的官方文档。
有关使用 Java 创建 Lambda 函数的更多详细信息,您可以查看我们的文章AWS Lambda With Java 。
6.1. 我们的开发环境的配置
首先,我们需要为Amazon Linux 2拉取 Docker 映像并安装Amazon Corretto:
# download Amazon Linux
docker pull amazonlinux
# inside the Docker container, install Amazon Corretto
yum install java-11-amazon-corretto
# some additional libraries needed for jaotc
yum install binutils.x86_64
6.2. 编译类和库
在我们的 Docker 容器中,我们执行以下命令:
# create folder aot
mkdir aot
cd aot
mkdir jaotc
cd jaotc
文件夹的名称只是一个示例,当然可以是任何其他名称。
package jaotc;
public class JaotCompilation {
public static int message(int input) {
return input * 2;
}
}
下一步是编译类和库:
javac JaotCompilation.java
cd ..
jaotc -J-XX:+UseSerialGC --output jaotCompilation.so jaotc/JaotCompilation.class
在这里,使用与 AWS 上相同的垃圾收集器很重要。如果我们的库无法在 AWS Lambda 上加载,我们可能希望使用以下命令检查实际使用的垃圾收集器:
java -XX:+PrintCommandLineFlags -version
现在,我们可以创建一个包含我们的库和类文件的 zip 文件:
zip -r jaot.zip jaotCompilation.so jaotc/
6.3. 配置 AWS Lambda
最后一步是登录 AWS Lamda 控制台,上传 zip 文件并使用以下参数配置 Lambda:
- 运行时:Java 11
- 处理程序:jaotc.JaotCompilation::message
此外,我们需要创建一个名为 JAVA_TOOL_OPTIONS 的环境变量并将其值设置为:
-XX:+UnlockExperimentalVMOptions -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so
这个变量允许我们将参数传递给 JVM。
最后一步是为我们的 Lambda 配置输入。默认是 JSON 输入,不能传递给我们的函数,因此我们需要将其设置为包含整数的字符串,例如“1”。
最后,我们可以执行我们的 Lambda 函数,并且应该在日志中看到我们的 AOT 编译库已加载:
57 1 loaded ./jaotCompilation.so aot library