1. 简介
在本文中,我们将介绍 Java 的提前编译(Ahead-of-Time Compilation,简称 AOT)。这项技术在 JEP-295 中被定义,并作为实验性功能首次引入 Java 9。
我们将从以下几个方面展开:
- 什么是 AOT 编译;
- 通过一个简单的示例展示 AOT 的使用;
- AOT 的一些限制;
- AOT 的典型应用场景。
2. 什么是 AOT 编译?
✅ AOT 编译是一种用于提升 Java 程序性能,特别是 JVM 启动速度的技术。
JVM 默认执行的是 Java 字节码,并在运行时将热点代码编译为本地机器码,这个过程称为即时编译(JIT)。JIT 的优势在于可以根据运行时信息进行高度优化,从而提升程序峰值性能。
❌ 但 JIT 的缺点也很明显:程序启动初期性能较差,因为此时热点代码尚未被编译。
💡 AOT 编译的目的就是解决这个“预热期”问题。它在程序运行前就将字节码编译为本地代码,从而缩短启动时间。
⚠️ AOT 编译器使用的是 Graal 编译器。关于 JIT 和 Graal 的更多细节,可以参考我们其他文章:
3. 示例演示
我们通过一个简单的 Java 类来演示 AOT 的使用。
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'";
}
}
首先,使用 javac
编译该类:
javac JaotCompilation.java
然后使用 jaotc
编译器生成本地库文件:
jaotc --output jaotCompilation.so JaotCompilation.class
这会生成一个名为 jaotCompilation.so
的本地共享库文件。
3.2. 运行程序
执行时,通过 -XX:AOTLibrary
参数加载 AOT 库:
java -XX:AOTLibrary=./jaotCompilation.so JaotCompilation
也可以将库文件拷贝到 $JAVA_HOME/lib
目录下,然后直接使用库名。
3.3. 验证库是否被使用
添加 -XX:+PrintAOT
参数可以查看库是否被加载:
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
文件一致,否则不会使用 AOT 代码。
如果我们修改了类代码,例如:
public static String message() {
return "The JAOT compiler says 'Good morning'";
}
但未重新编译 AOT 库,运行时将不使用 AOT 库中的方法:
11 1 loaded ./jaotCompilation.so aot library
The JAOT compiler says 'Good morning'
4. 更多 AOT 与 JVM 参数
4.1. 编译 Java 模块
我们可以对整个模块进行 AOT 编译:
jaotc --output javaBase.so --module java.base
生成的库文件通常很大(约 320MB),可以通过指定包或类来减小体积。
4.2. 使用编译命令进行选择性编译
为避免库文件过大,可以使用编译命令文件限制编译范围。例如创建 compileCommands.txt
文件:
compileOnly java.lang.*
然后使用:
jaotc --output javaBaseLang.so --module java.base --compile-commands compileCommands.txt
生成的库只包含 java.lang
包下的类。
💡 要提升性能,可以通过以下参数获取 JVM 预热时调用的类:
java -XX:+UnlockDiagnosticVMOptions -XX:+LogTouchedMethods -XX:+PrintTouchedMethodsAtExit JaotCompilation
4.3. 编译单个类
jaotc --output javaBaseString.so --class-name java.lang.String
仅编译 String
类。
4.4. 编译支持 Tiered 模式
默认情况下,AOT 编译的代码始终使用,不会进行 JIT。如果希望支持 JIT,可以添加 --compile-for-tiered
参数:
jaotc --output jaotCompilation.so --compile-for-tiered JaotCompilation.class
此时,AOT 代码将被使用直到 JIT 编译就绪。
5. AOT 编译的典型使用场景
✅ 适用于启动时间敏感的短生命周期程序,如命令行工具、脚本等。
✅ 嵌入式系统或资源受限环境,JIT 无法使用时 AOT 是一个备选方案。
⚠️ AOT 库只能被字节码完全一致的类加载,不能通过 JNI 加载。
6. AOT 与 AWS Lambda
✅ AOT 编译非常适合 AWS Lambda 函数,尤其是对启动时间敏感的场景。
⚠️ 必须在与 AWS Lambda 相同的操作系统(如 Amazon Linux 2)上构建 AOT 库。
6.1. 开发环境配置
使用 Docker 搭建 Amazon Linux 2 + Amazon Corretto 环境:
docker pull amazonlinux
yum install java-11-amazon-corretto
yum install binutils.x86_64
6.2. 编译类与库
在容器中编译并生成库:
mkdir aot && cd aot
mkdir jaotc && cd jaotc
# JaotCompilation.java 内容如下:
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
zip -r jaot.zip jaotCompilation.so jaotc/
⚠️ 请确保使用与 AWS Lambda 相同的 GC(如 SerialGC)。
6.3. 配置 AWS Lambda
上传 jaot.zip
并设置如下参数:
- Runtime: Java 11
- Handler:
jaotc.JaotCompilation::message
- Environment Variable:
- Name:
JAVA_TOOL_OPTIONS
- Value:
-XX:+UnlockExperimentalVMOptions -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so
- Name:
输入测试数据为字符串 "1"
,执行后可在日志中看到:
57 1 loaded ./jaotCompilation.so aot library
7. 总结
本文我们演示了如何使用 jaotc
对 Java 类和模块进行 AOT 编译,并探讨了其在 Lambda 等场景中的应用。
⚠️ AOT 目前仍为实验性功能,不是所有 Java 发行版都默认包含,实际应用案例也较为稀少。未来能否成为主流,还需要社区进一步探索和实践。