1996 年 JDK 1.0 发布,同年 7 月 外挂即时编译器发布(JDK 1.0.2),而 Java 提前编译发布在之后几个月(IBM High Performance Compiler for Java),1998 年 GNU 组织公布 GCC 家族新成员 GNU Compiler for Java(GCJ,2018 年从 GCC 家族除名),在 OpenJDK 流行起来之前,GCJ 是 Linux 发行版自带的 Java 实现版本;
AOT Compiler 因丢失了平台中立性而在 Java 世界沉寂长达 15 年;至 2013 年,因 Android 提前编译的 ART(Andriod Runtime)蹂躏式的取代即时编译的 Dalvik VM(Andriod 4.4),大大震撼了 Java 世界,Java AOT Compiler 重新被翻了出来;
提前编译就是以平台中立性、字节膨胀(提前编译的本地二进制体积明显大于字节码体积)、动态扩展(提前编译要求程序封闭,不能外部动态加载新的字节码)等特性换取足够好的性能;
文章目录
- 1. 提前编译的优缺点
- 2. Jaotc 提前编译
1. 提前编译的优缺点
提前编译的两种方案
方案一
,在程序运行之前把程序代码编译成机器码的静态翻译(类似传统 C、C++ 编译器,Substrate VM
);- 优势在于,提前编译可以放心大胆的采用
全程序优化
措施以获取更好的运行时性能;即时编译所消耗的时间和资源是原本可以用来运行程序的时间,这导致即使编译不得不放松一些全程序分析
(Inter-Procedural Analysis,IPA,或称过程间分析;如分析变量的值是否一定为常量、代码块是否永远不会被执行,虚方法的调用是否只会存在一个版本,这些分析对流敏感、对路径敏感、对上下文敏感、对字段敏感,需要大量计算工作;目前 JVM 大多使用方法内联达到过程内分析的效果,或借助假设激进优化,不求精确结果);
- 优势在于,提前编译可以放心大胆的采用
方案二
,将原本即时编译器运行时编译要做的事提前做好并保存下来,下次运行到这些代码时直接加载并运行(如公共库代码在同一机器被多个 Java 进程使用);(Jaotc
);- 相当于给即时编译做缓存加速,又称动态提前编译(Dynamic AOT)或即时编译缓存(JIT Caching),已被主流商用 JDK 完全支持(CDS,Class Data Sharing,编译质量低于即时编译);基于 Graal 编译器实现的 Jaotc 提前编译器会针对目标机器为应用程序做较高质量优化的提前编译,然后 HotSpot 在运行时可以直接加载运行(快启动、快响应);
即时编译的优势
- 性能分析制导优化(Profile-Guided Optimization,PGO),即时编译会在解释器或客户端编译器运行过程中收集性能监控信息,从而做出明显偏好性资源分配(如抽象类的实际类型、判断条件走哪个分支、方法调用的实际版本、循环会走多少次等,在静态分析中是无法得到或无法唯一确认的,通过这些可以实现集中优化和偏好资源分配,如分支预测、寄存器、缓存等资源);
- 激进预测性优化(Aggressive Speculative Optimization),静态优化必须保证优化前后执行结果等效;而即时编译可以不那么保守,只要监控信息支持,它可以大胆执行预测性的优化,从而大幅度降低目标代码复杂度、提升运行速度;
- 链接时优化(Link-Time Optimization,LTO),提前编译的主程序与动态链接库的代码在他们编译时完全独立,两者各自编译、优化,存在明显的边界隔阂(比如方法内联会很困难);而即时编译在对 Class 文件做优化时是基于程序整体的;
2. Jaotc 提前编译
Jaotc 支持对 Class 文件和模块进行提前编译,但这些功能必须针对特定物理机器和目标 VM Arguments
;
HelloWorld 提前编译演示
# 前端编译,生成 HelloWorld.class
$ javac HelloWorld.java
# 直接通过 HelloWorld.class 运行
$ java HelloWorld# 提前编译 HelloWorld.class
$ jaotc --output libHelloWorld.so HelloWorld.class# 确认 libHelloWorld.so 是否一个静态链接库
$ ldd libHelloWorld.so
statically linked# 查看 HelloWorld 的构造函数和 main 方法
$ nm libHelloWorld.so
...
0000000000002a20 t HelloWorld.()V
0000000000002b20 t HelloWorld.main([Ljava/lang/String;)V
...# 通过静态链接库(而非 HelloWorld.class)运行
java -XX:AOTLibrary=./libHelloWorld.so HelloWorld
java.base 模块提前编译演示
- 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;
# -J 参数传递与目标 VM Arguments 相关的运行时参数;
$ 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
Compiling libjava.base-coop.so...
6177 classes found (335 ms)
55845 methods total, 49575 methods to compile (1037 ms)
Compiling with 4 threads
......
49575 methods compiled, 0 methods failed (138821 ms)
Parsing compiled code (906 ms)
Processing metadata (10867 ms)
Preparing stubs binary (0 ms)
Preparing compiled binary (103 ms)
Creating binary: libjava.base-coop.o (2719 ms)
Creating shared library: libjava.base-coop.so (5812 ms)
Total time: 163609 ms# 用 -XX:AOTLibrary 指定链接库位置,运行 HelloWorld
$ java -XX:AOTLibrary=java_base/libjava.base-coop.so, ./libHelloWorld.so HelloWorld
Hello World!# 用 -XX:+PrintAOT 打印使用了提前编译的方法
$ java -XX:+PrintAOT -XX:AOTLibrary=./libHelloWorld.so HelloWorld11 1 loaded ./libHelloWorld.so aot library105 1 aot[ 1] HelloWorld.()V105 2 aot[ 1] HelloWorld.main([Ljava/lang/String;)V
Hello World!
Jaotc 还难以直接编译 SpringBoot、MyBatis 等常见的第三方库,Java 标准库也只能比较顺利的编译 java.base 模块;Graal 编译器也无法支持 ZGC 与 Shenandoah GC,只能支持 G1 和 Parallel(PS + PS Old);未来很长时间 Java 后端编译的主角应该还是即时编译
;
上一篇:「JVM 编译优化」即时编译器
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》