aot从入门到实战

aot介绍


aot是Ahead-Of-Time的缩写,以前大家都知道java的一个定位就是半编译,半解释型语言。他把java文件编译成class文件,最后jvm解释执行class文件,jvm可以把class文件解释为对应的机器码,这个就是靠的jit。aot则是直接把class文件编译系统的库文件,不在依靠jit去做这个事情。

第一个aot的程序


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  list

  list       A : separated list of class names, modules, jar files
             or directories which contain class files.

where options include:
  --output             Output file name
  --class-name  List of classes to compile
  --jar            List of jar files to compile
  --module          List of modules to compile
  --directory          List of directories where to search for files to compile
  --search-path        List of directories where to search for specified files
  --compile-commands   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 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                   Pass  directly to the runtime system

查看帮助文档,发现他支持的扫描方式多种多样,最简单的方式,我们依旧使用maven编译,最后打成的jar包。通过--jar参数来生成库文件。

aot的限制


启动参数固定化

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

发现是成功的。
启动的参数需要固定化
这也就是说你有两种配置,就得先用jaotc去编译出两个版本的库,在用的时候进行选择。

编译参数和启动参数要一致

我们基于上面产生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.()V
    113    2     aot[ 1]   AotTest.main([Ljava/lang/String;)V

发现ps是可以打印出aot的方法的。

class文件是必须存在

我们既然编译出可执行文件了,那么class文件是不是可以不要了呢。答案是不行的。我们下面用一个复杂一点的案例。

public class TestB{
    public static void main(String[] args) {
        System.out.println("this is TestB");
    }
}

准备一个调用类。
然后在另外一个文件里调用B。

public class AotTest{
    public static void main(String[] args) {
        TestB.main(null);
    }
}

我们这次把B单独编译成库,依旧使用。

jaotc --output  libB.so TestB.class

现在删除TestB.class,再次执行。我们发现会报下面的错误。

Exception in thread "main" java.lang.NoClassDefFoundError: TestB
    at AotTest.main(AotTest.java:3)
Caused by: java.lang.ClassNotFoundException: TestB
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    ... 1 more

直接是NoClassDefFoundError。

所以aot的存在是替换了jit的部分,并不代表所有的数据都在生成的二进制文件里。aot并不是把java变成了例如c那种的可执行文件,他只是不需要动态的编译了,直接拿到现成的结果。

解惑


既然aot是一种固化,那么对动态的一些特性是否有影响呢。

aot与反射

我写了一个比较简单的demo,发现aot是支持反射的。

import java.lang.reflect.Field;

public class AotTest {
    public static void main(String[] args) throws Exception {
        Class<?> testB=Class.forName("TestB");
           Field fields = testB.getDeclaredField("SS");
            fields.setAccessible(true);
              String ss = (String) fields.get(null);
              System.out.println(ss);

    }
}

public class TestB{

    private final static String SS="ggg";
    public static void main(String[] args) {
        System.out.println("this is TestB");
    }
}

我们把两个class文件打到一个库里。

jaotc  --output libtest.so AotTest.class TestB.class

执行我们看到,TestB也是可以正常执行的。

java  -XX:+PrintAOT  -XX:AOTLibrary=./libtest.so AotTest
    747    1     loaded    ./libtest.so  aot library
    828    1     aot[ 1]   AotTest.()V
    828    2     aot[ 1]   AotTest.main([Ljava/lang/String;)V
    828    3     aot[ 1]   TestB.()V
    828    4     aot[ 1]   TestB.main([Ljava/lang/String;)V
ggg

aot与lambda

import java.lang.reflect.Field;

public class AotTest {
    public static void main(String[] args) throws Exception {
        Class&lt;?&gt; testB=Class.forName("TestB");
           Field fields = testB.getDeclaredField("SS");
            fields.setAccessible(true);
              String ss = (String) fields.get(null);
              System.out.println(ss);


              MathOperation addition = (int a, int b) -> a + b;
              addition.operation(1, 2);
    }
}

interface MathOperation {
    int operation(int a, int b);
}

沿用上面的例子,加了一个lambda。我们看看结果。

$ java  -XX:+PrintAOT  -XX:AOTLibrary=./libtest.so AotTest
    673    1     loaded    ./libtest.so  aot library
    752    1     aot[ 1]   AotTest.lambda$main$0(II)I
    752    2     aot[ 1]   AotTest.()V
    752    3     aot[ 1]   AotTest.main([Ljava/lang/String;)V
    753    4     aot[ 1]   TestB.()V
    753    5     aot[ 1]   TestB.main([Ljava/lang/String;)V
ggg

我们发现lambda也是可以正常运行的。

lambda是动态生成字节码的典型了,是不是动态生成的,aot也会全量支持呢。我们下面来验证这个问题。
lambda是核心类库,所以这次我们把核心类库也编译了。以防误差。

jaotc --output libjava.base.so --module java.base

编写一个lambda的简单demo。

public class AotTest {
    public static void main(String[] args) throws Exception {
              MathOperation addition = (int a, int b) -> a + b;
              addition.operation(1, 2);
    }
}

interface MathOperation {
    int operation(int a, int b);
}

我们可以通过增加参数,把动态生成的字节码打印成文件。利用-Djdk.internal.lambda.dumpProxyClasses。
利用类加载来打印类加载信息。-verbose:class。
打印aot的信息。-XX:+PrintAOT
把程序输出重定向到一个文件里去。

java  -verbose:class  -XX:+PrintAOT -Djdk.internal.lambda.dumpProxyClasses=.  -XX:AOTLibrary=./libtest.so,./libjava.base.so AotTest > 1.txt

我把动态生成的字节码就放在当前路径了,我们可以看到文件夹下多了一个AotTest$$Lambda$1.class。
我们在日志中可以搜到类加载信息。

[0.339s][info][class,load] AotTest$$Lambda$1/0x0000000800067840 source: AotTest

但是搜不到aot的信息。
说明aot本身对于动态生成的字节码无法预先处理,哪怕他是jdk的核心类库。

aot与动态注入

如果使用了-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.()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.()V
    107    3     aot[ 1]   AotTest.main([Ljava/lang/String;)V
    108    4     aot[ 1]   TestB.()V
    108    5     aot[ 1]   TestB.main([Ljava/lang/String;)V
this is TestB

结果发现testB不见了,也就是说agent注入改造后的类是无法使用aot的效果的。

实战


对核心类库编译

java核心类库可以说是调用比较频繁的代码,所以把核心类库进行编译,可以有效的提升启动速度。

jaotc --output libjava.base.so --module java.base

一般情况下,jdk是保证了核心类库的编译是没有问题的。上面的指令除非用的预览版本,那都是正常的。

合理设置参数

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,这个错误并不会让程序失败停止。还需要做日志分析。所以说这个用起来确认正确性还真是一个麻烦的事情。

实战编译math3
我们下面编译math3来展示常用的方式。
我们先尝试编译

jaotc  --output libmath.so --jar commons-math3-3.6.1.jar

你会发现这个会报错。

Error: Failed compilation: org.apache.commons.math3.optim.nonlinear.scalar.noderiv.BOBYQAOptimizer.bobyqb([D[D)D: org.graalvm.compiler.core.common.PermanentBailoutException: Too many loops in method
Error: Failed compilation: org.apache.commons.math3.optimization.direct.BOBYQAOptimizer.bobyqb([D[D)D: org.graalvm.compiler.core.common.PermanentBailoutException: Too many loops in method

这个情况是我们比较常见的,aot现在还不能匹配所有的场景。我们最简单的就是去掉这个,保证我们编译成功。我们可以使用exclude来去掉不可编译选项。
编写一个文件命名随意,我这里用 exclude.txt,把不能编译的方法都做一下去除。

exclude org.apache.commons.math3.optim.nonlinear.scalar.noderiv.BOBYQAOptimizer.bobyqb([D[D)D
exclude org.apache.commons.math3.optimization.direct.BOBYQAOptimizer.bobyqb([D[D)D

这次尝试编译的话,我们使用--compile-commands把我们指定的规则加上去。

jaotc  --output libmath.so  --compile-commands  exclude.txt --jar commons-math3-3.6.1.jar

再次执行,可以生成对应的libmath.so。
exclude掉一些方法,只要那个不是什么热点方法,其实影响不会太大。我们只要保证大部分的,热点的代码,可以利用aot加速,程序的启动速度就会得到很大的提升。所以这里并不用太担心影响。
有exclude,所以同时也有compileOnly。目的就是比较好的选择出可以编译的选项。

常用参数

  --compile-for-tiered       Generate profiling code for tiered compilation

在上面介绍的一些参数外,一般编译时还加上面这个参数。这个参数上标注了以后可能会去除,说明那时候aot有更好的解决方案了。在没有对应的解决方案出来时,先都加着吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容