aot介绍
aot是Ahead-Of-Time的缩写,以前大家都知道java的一个定位就是半编译,半解释型语言。他把java文件编译成class文件,最后jvm解释执行class文件,jvm可以把class文件解释为对应的机器码,这个就是靠的jit。aot则是直接把class文件编译系统的库文件,不在依靠jit去做这个事情。并不是说编译好本地库以后,原来的class就可以不要删除了。
demo环境
要求 | 版本 |
系统 | macos |
java | jdk11 |
本地编译 | xcode11 |
这里提到了系统和编译器。可以说写c需要什么,这里就需要准备什么,简单点的话,最好准备和系统配套的编译器,减少编译的坑。
java代码
public class AotTest{
public static void main(String[] args) {
System.out.println("first aot");
}
}
平平无奇,和我们普通写java一样。 编译java文件
javac AotTest.java
平平无奇,和我们平时编译也没区别。 开始编译成本地库
jaotc --output libtest.so AotTest.class
jaotc是jdk提供的编译成本地库的方式。他和java一样,在bin目录下,如果配置了环境变量,是可以执行的。 如果系统和编译器匹配,这里就是一把过。最终会在本地生成libtest.so的文件。 开始执行
java -XX:AOTLibrary=./libtest.so AotTest
编译成本地库,我们也还是要指定一下main的类,这个和以前java -cp执行没有区别。
first aot
不出意外可以执行方法。 你一定也发现了,这个操作很繁琐,这还是一个文件,要是编译出来的class特别多,怎么办。而且还没有管理的工程文件。 这点jdk已经帮我们想到了。
jaotc
Usage: jaotc <options> list
list A : separated list of class names, modules, jar files
or directories which contain class files.
where options include:
--output <file> Output file name
--class-name <class names> List of classes to compile
--jar <jarfiles> List of jar files to compile
--module <modules> List of modules to compile
--directory <dirs> List of directories where to search for files to compile
--search-path <dirs> List of directories where to search for specified files
--compile-commands <file> Name of file with compile commands
--compile-for-tiered Generate profiling code for tiered compilation
--compile-with-assertions Compile with java assertions
--compile-threads <number> Number of compilation threads to be used
--ignore-errors Ignores all exceptions thrown during class loading
--exit-on-error Exit on compilation errors
--info Print information during compilation
--verbose Print verbose information
--debug Print debug information
-? -h --help Print this help message
--version Version information
--linker-path Full path to linker executable
-J<flag> Pass <flag> directly to the runtime system
查看帮助文档,发现他支持的扫描方式多种多样,最简单的方式,我们依旧使用maven编译,最后打成的jar包。通过--jar参数来生成库文件。
启动参数固定化
jaotc可以通过加-J参数来指定jvm的启动参数。 我们尝试使用cms来编译一下库文件。
jaotc -J-XX:+UseConcMarkSweepGC --output libtest.so AotTest.class
执行的结果会有两条信息。
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
Error occurred during initialization of VM
JVMCI Compiler does not support selected GC: concurrent mark sweep gc
第一条是cms已经标记为废弃。第二条是jvmci不支持cms。 按照官方文档上讲,现在的aot支持ps和g1。其他的并不支持。 我们下面试试ps,因为现在默认已经是g1了。
jaotc -J-XX:+UseParallelGC --output libtest.so AotTest.class
发现是成功的。 我们基于上面产生ps的libtest.so,我们尝试换个启动参数。
java -XX:+UseConcMarkSweepGC -XX:AOTLibrary=./libtest.so AotTest
java -XX:+UseParallelGC -XX:AOTLibrary=./libtest.so AotTest
java -XX:+UseG1GC -XX:AOTLibrary=./libtest.so AotTest
你会发现上面三个启动参数都会执行正确,并没有报错。 是不是感觉违背了官方讲的编译和启动一致的这个要求。 这里介绍一个参数。
-XX:+PrintAOT
这个参数可以打出使用aot的klasses和method。我们下面再试试G1(库是上面指定了ps的)。
java -XX:+UseG1GC -XX:+PrintAOT -XX:AOTLibrary=./libtest.so AotTest
我们会发现有不一样的输出。
Shared file ./libtest.so error: used 'parallel gc' is different from current 'g1 gc'
7 1 skipped ./libtest.so aot library
这里会有一个错误提示,说libtest.so是使用了ps和现在用的g1不一样。跳过了这个库。 然后对比一下ps的结果。
12 1 loaded ./libtest.so aot library
113 1 aot[ 1] AotTest.<init>()V
113 2 aot[ 1] AotTest.main([Ljava/lang/String;)V
发现ps是可以打印出aot的方法的。
合理设置参数
jaotc可以使用-J指定运行时参数,官方的例子中使用了gc参数和压缩指针参数。
jaotc -J-XX:-UseCompressedOops --output libtest.so AotTest.class
如果运行时没有-XX:-UseCompressedOops日志中会打印出一个异常。
Shared file ./libtest.so error: UseCompressedOops has different value 'false' from current 'true'
832 1 skipped ./libtest.so aot library
gc的也同理。 哪些运行时参数是必须编译时就设置的,这个我自己测试了-Xmx这些,是可以运行的。现在发现的其实就是那两个参数,这个只能说实践中慢慢确认了。 这里也就是出现了一个问题,我们的程序不一定都要用G1,有的也需要使用ps,堆小的是需要开启压指针的,堆大的确实不需要。针对这种情况,我们能做的就是把情况和组合枚举一下,然后编译出多个版本,启动的时候指定不同的版本,官方就推荐这么做的,甚至他还举了例子。
-XX:-UseCompressedOops -XX:+UseG1GC : libjava.base.so
-XX:+UseCompressedOops -XX:+UseG1GC : libjava.base-coop.so
-XX:-UseCompressedOops -XX:+UseParallelGC : libjava.base-nong1.so
-XX:+UseCompressedOops -XX:+UseParallelGC : libjava.base-coop-nong1.so
应该庆幸参数可能就这么少,如果运行时特别多的话,编译起来估计要疯的,得写脚本做循环遍历。然后用的时候得按照规则加载出合适的库,大部分时间都花在了脚本匹配上了,而且还得打开-XX:+PrintAOT,这个错误并不会让程序失败停止。还需要做日志分析。所以说这个用起来确认正确性还真是一个麻烦的事情。
动态注入
如果使用了-javaagent加入的监控修改了字节码会是什么表现呢? 我们使用了字节码注入的agent demo。下面是个开源版本。 注入agent 这里一定要注意一个点,这个工具是通过asm做的,他可以打印方法的运行时间。**使用时要把asm的jar包换成一个对应jdk的版本,目前项目用的6,java11得升级。**否则你会发现神奇的错误,那个错误妙不可言。
java -Xbootclasspath/a:asm-8.0.1.jar:asm-analysis-8.0.1.jar:asm-commons-8.0.1.jar:asm-tree-8.0.1.jar -javaagent:trace-0.0.1-SNAPSHOT-agent.jar=Test -XX:AOTLibrary=./libtest.so -XX:+PrintAOT AotTest
通过这个启动参数。我这里只注入我的一个测试类。看看他的方法打印的结果,以及aot的表现。 加agent日志
179 1 aot[ 1] AotTest.lambda$main$0(II)I
179 2 aot[ 1] AotTest.<init>()V
this is TestB
[Ljava.lang.String; main cost 0(这里是agent输出,单位是毫秒)
不加agent
11 1 loaded ./libtest.so aot library
107 1 aot[ 1] AotTest.lambda$main$0(II)I
107 2 aot[ 1] AotTest.<init>()V
107 3 aot[ 1] AotTest.main([Ljava/lang/String;)V
108 4 aot[ 1] TestB.<init>()V
108 5 aot[ 1] TestB.main([Ljava/lang/String;)V
this is TestB
结果发现testB不见了,也就是说agent注入改造后的类是无法使用aot的效果的。
小结
java发展这么多年了,大家也没有感觉jit有什么不好的,为什么还要加入aot呢。 我们如果对比jit和aot,可以发现一些区别,就是aot已经编译成本地代码了,不用再靠jvm进行解释执行,这样省去了jvm的解释时间。换句话说启动更快了。 启动更快的这个需求,可以说是伴随云原生的。我们设想以下的场景。web服务进行促销业务,结果发现流量比预期的多,刚来时就达到了警戒线,因为是突发,所以增长速度要比原来快。此时启动一个程序花了5s,那么就得有5s的风险去承担流量冲击,很可能把服务冲垮了,如果1s就可以启动成功,那么风险就会大大减小。 aot肯定不完全都是好的,跨平台的特性首选去除了。其次aot最终运行效果是不如jit的。jit因为在运行时进行分析,所以更好的了解程序的运行,aot则不能,所以启动更快,但是巅峰的运行速度,可能是略差与jit的。并且为了编译成功,依赖的jar包必须都全部在,例如编译A,那么A依赖的库必须都在,否则编译会失败,反射,aop等动态的特性都会有问题。A依赖B里的一个逻辑,B的方法里依赖了C,jit只有在方法调用的时候才会触发类加载等,A调用的方法里不用C,C是可以不加入的。但是编译成本地库则不能,不管用不用,依赖就得在。