提前编译(AoT)

2023/06/09

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。请参阅我们的其他文章,了解Java9和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大小约为320MB,并且加载需要一些时间,可以通过选择要AOT编译的包和类来减小大小。我们将在下面介绍如何做到这一点,但是,我们不会深入讨论所有细节。

4.2 使用编译命令进行选择性编译

为了防止Java模块的AOT编译库变得太大,我们可以添加编译命令来限制AOT编译的范围。这些命令需要在一个文本文件中,在我们的示例中,我们将使用文件complileCommands.txt:

compileOnly java.lang.

然后,我们将其添加到compile命令中:

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 compile-for-tiered

默认情况下,将始终使用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和亚马逊Lambda

AOT编译代码的一个可能用例是短寿命的lambda函数,其中短启动时间很重要。在本节中,我们将了解如何在AWS Lambda上运行AOT编译的Java代码。

将AOT编译与AWS Lambda一起使用需要在与AWS上使用的操作系统兼容的操作系统上构建库。在撰写本文时,这是Amazon Linux 2。

此外,Java版本需要匹配,AWS提供了Amazon CorrettoJava11 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:

  • Runtime:Java 11
  • Handler: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

7. 总结

在本文中,我们了解了如何AOT编译Java类和模块。由于这仍然是一个实验性功能,AOT编译器并不是所有发行版的一部分;真正的例子仍然很少见,这将取决于Java社区来找出应用AOT的最佳用例。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章