Contents

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 2Amazon 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