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是可以不加入的。但是编译成本地库则不能,不管用不用,依赖就得在。