• Java在云原生的破局利器——AOT(JIT与AOT)


    导读

    JIT(Just-in-Time,实时编译)一直是Java语言的灵魂特性之一,与之相对的AOT(Ahead-of-Time,预编译)方式,似乎长久以来和Java语言都没有什么太大的关系。但是近年来随着Serverless、云原生等概念和技术的火爆,Java JVM和JIT的性能问题越来越多地被诟病,在Golang、Rust、NodeJS等新一代语言的包夹下,业界也不断出现“云原生时代,Java已死”的言论。那么,Java是否可以使用AOT方式进行编译,摆脱性能的桎梏,又是否能够在云原生时代焕发新的荣光?本文会带着这样的疑问,去探索Java AOT技术的历史和现状。

    上上篇有讲过,HotSpot JVM中集成了两种JIT编译器,Client Compiler和Server Compiler,它们的作用也不同。Client Compiler注重启动速度和局部的优化,Server Compiler则更加关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢。两种编译器有着不同的应用场景,在虚拟机中同时发挥作用。而随着时间的发展,不论是Client Compiler还是Server Compiler都发展出了各具特色的实现,如 C1、C2、Graal Compiler等,你可以在JVM启动参数中选择自己所需的JIT编译器实现。

    从JDK 10起,HotSpot虚拟机同时拥有三种不同的即时编译器。此前我们已经介绍了经典的客
    户端编译器和服务编译,还有全新的即时编译器:Graal编译器。

    JIT与AOT的区别

    提前编译是相对于即时编译的概念,提前编译能带来的最大好处是Java虚拟机加载这些已经预编译成二进制库之后就能够直接调用,而无须再等待即时编译器在运行时将其编译成二进制机器码。理论上,提前编译可以滅少即时编译带来的预热时间,减少Java应用长期给人带来的“第一次运行慢"的不良体验,可以放心地进行很多全程序的分析行为,可以使用时间压力更大的优化措施。但是提前编译的坏处也很明显,它破坏了Java"—次编写,到处运行"的承诺,必须为每个不同的硬件、操作系统去编译对应的发行包;也显著降低了Java链接过程的动态性,必须要求加载的代码在编译期就是全部已知的,而不能在运行期才确定,否则就只能舍弃掉己经提前编译好的版本,退回到原来的即时编译执行状态。

    AOT的优点

    • 在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
    • 可以在程序运行初期就达到最高性能,程序启动速度快
    • 运行产物只有机器码,打包体积小

    AOT的缺点

    • 由于是静态提前编译,不能根据硬件情况或程序运行情况择优选择机器指令序列,理论峰值性能不如JIT
    • 没有动态能力
    • 同一份产物不能跨平台运行

    Java AOT的历史演进

    JIT是Java的一大灵魂特性,得益于即时编译,Java语言同时拥有了不输编译型语言的运行速度和“一次编译、到处运行”的跨平台能力、甚至还拥有和解释型语言类似的动态性能力,可以说JIT是Java语言能够快速风靡全球并得到广泛应用的重要原因之一。因此在Java诞生至今的几十年里,AOT编译方式和Java可以说是“一毛钱关系都没有”,那么为什么今天我们又要提起以AOT的方式运行Java程序呢,是JIT它不香么?

    其实Java本身一直存在着一些“问题”:JVM本身是很重的,因此对服务器的性能消耗(某种意义上可以说是性能浪费)是很高的,同时Java应用的启动速度也往往被人所诟病。但是这些问题在Java所带来的跨平台运行能力和动态特性面前,都是“值得的牺牲” —— 使用Java你可以更方便的进行代码的打包和交付,可以轻松写出性能不差的程序并部署在任何主流的OS上。这些对于企业用户而言,一直是技术选型非常重要的考量因素,直到Docker和Serverless的诞生,逐渐改写了这一切:

    Docker的诞生,让底层运行环境变得可以随意定制,你可以在生产环境的任何一台服务器上轻松混部Windows和Linux的各种发行版,这让JVM的跨平台能力显得不那么重要了。

    Serverless概念的火爆,让弹性伸缩能力成为服务端程序的一大重要目标,这时候JVM的“臃肿”和JIT导致的启动延迟就让Java程序显得很不“Serverless”(拉包时间长、启动速度慢),如今我们提到云原生总是先想到Go、NodeJS而不是Java,似乎Java和云原生已经不是一个时代的产物了。

    所以如果想让Java在云原生时代焕发“第二春”,支持AOT是非常重要的一步,而在这一步上,Java语言却经历了一波三折:

    2016年,OpenJDK的 JEP 295提案首次在Java中引入了AOT支持,在这一草案中,JDK团队提供了 jaotc 工具,使用此工具可以将指定class文件中的方法逐个编译到native代码片段,通过Java虚拟机在加载某个类后替换方法的的入口到AOT代码来实现启动加速的效果。

    jaotc的类似于给JVM打了一个“补丁”,让用户有权利将部分代码编译成机器码的时期提前,并预装载到JVM中,供运行时代码调用。不过这个补丁存在很多问题:

    首先是在设计上没有考虑到Java的多Classloader场景,当多个Classloader加载的同名类都使用了AOT后,他们的static field是共享的,而根据java语言的设计,这部分数据应该是隔开的。由于这个问题无法快速修复,jaotc最终给出的方案只是暴力地禁止用户自定义classloader使用AOT。

    此外,由于社区人手不足,缺乏调优和维护,jaotc的实际运行效果不尽人意,有时甚至会对应用的启动和运行速度带来反向优化,实装没多久之后就退化为实验特性,最终在JDK 16中被删除,结束了短暂的一生。

    后来阿里AJDK团队自研的AppCDS(Class-Data-Share)技术继承了jatoc的思路,进行了大幅的优化和完善,目前也不失为一种Java AoT的选择,其本质思路和jaotc基本一致 ,这里就不再赘述了。

    而目前业界除了这种在JVM中进行AOT的方案,还有另外一种实现Java AOT的思路,那就是直接摒弃JVM,和C/C++一样通过编译器直接将代码编译成机器代码,然后运行。这无疑是一种直接颠覆Java语言设计的思路,不过还是被各路大佬们实现了,那就是GraalVM Native Image。它通过C语言实现了一个超微缩的运行时组件 —— Substrate VM,基本实现了JVM的各种特性,但足够轻量、可以被轻松内嵌,这就让Java语言和工程摆脱JVM的限制,能够真正意义上实现和C/C++一样的AOT编译。这一方案在经过长时间的优化和积累后,已经拥有非常不错的效果,基本上成为Oracle官方首推的Java AOT解决方案,接下来我们会重点分析一下这项技术的原理和实际应用。

    新的破局点GraalVM

    先说一下GraalVM,这是Oracle在2019年推出的新一代UVM(通用虚拟机),它在HotSpotVM的基础上进行了大量的优化和改进,主要提供了两大特性:

    • Polyglot:多语言支持,你可以在GraalVM中无缝运行多种语言,包括Java、JS、Ruby、Python甚至是Rust。更重要的是可以通过GraalVM的API来实现语言混编 —— 比如在一段Java代码中无缝引用并调用一个Python实现的模块。
    • HighPerformance:高性能,首先它提供了一个高性能的JIT引擎,让Java语言在GraalVM上执行的时候效率更高速度更快 ;其次就是提供了SubstrateVM,通过Graal Compiler你可以将各种支持的语言(包括Java)编译成本地机器代码,获得更好的性能表现。

    值得一提的是,Substrate VM虽然名为VM,但并不是一个虚拟机,而是一个包含了 垃圾回收、线程管理 等功能的运行时组件(Runtime Library),就好比C++当中的stdlib一样。当Java程序被编译为Native Image运行的时候,并不是运行在Substrate VM里,而是将SubstrateVM当作库来使用其提供的各种基础能力,以保障程序的正常运行。

    不难看出,GraalVM这个项目的野心是非常大的,可以说这个项目是Oracle抢占云原生市场的一个重要布局,随着官方的不断投入和社区的壮大,目前GraalVM已经日渐成熟,在高性能和跨语言支持方面都交出了令人满意的答卷。GraalVM本身是一个非常庞大的项目,有很多的细节点可以深挖,不过接下来我们还是重点研究一下它的AOT能力 —— Native Image。

    Native Image:原理与限制

    一个Java程序究竟是如何被编译成静态可执行文件的?我们先来看一下NativeImage的原理。

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/9b4b6a56-e0fb-4ca8-bc9f-9a97b81769dd.png

    Native Image的输入是整个应用的所有组件,包括应用本身的代码、各种依赖的库、JDK库、以及SVM;首先会进行整个应用的初始化,也就是代码的静态分析,这个分析过程有点类似GC中的“可达性分析”,会讲程序运行过程中将所有可达的代码、变量、对象生成一个快照,最终打包成一个可执行的Native Image。

    一个完整的Native Image包含两个部分,一部分称为 Text Section,即用户代码编译成的机器代码;另一部分称为 Data Section,存储了应用启动后堆区内存中各种对象的快照。

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/d3065ea1-ee79-49fe-a845-c16726c3ef9a.png

    可以预见的是,这个静态分析的过程(官方称之为 Pionts-to Analysis)是非常复杂且耗时的,整个分析过程会以递归的方式进行,最终得到两个树形结构Call Tree(包含所有可达的方法)以及Object Tree(包含所有可达的对象),Call Tree中所包含的方法会被AOT编译为机器码,成为Native Image的Text Section,而Object Tree中所包含的对象及变量则会被保存下来,写入Native Image的Data Setion。

    整个静态分析的算法非常复杂,目前网上相关的资料也较少,如果有对具体算法感兴趣的同学,官方团队在Youtube上有一个相对比较详细的算法说明视频,可以自行查看:https://www.youtube.com/watch?v=rLP-8q3Cb8M

    作为一个Java程序员,你一定会好奇JVM的动态特性,例如反射、代理,要如何进行静态分析呢?很显然,这两者之间是存在冲突的,因此Native Image设置了一个名为“Closed World”的假设作为静态分析的基本前提。

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/5dc56b73-e2ab-414e-bf7a-da8bd2bd749c.png

    这个基本前提包含三个要求,对应的也就是目前Native Image存在的三个限制:

    1. Points-to分析的时候,需要接受完整的字节码作为输入(即项目中所有用到的class的字节码都需要获取的到)。

    => 在运行期动态生成或者是动态获取字节码的程序,无法构建成 Native Image。

    1. Java的动态特性,包括反射、JNI、代理,都需要通过配置文件在构建前实现声明好。

    => 无法提前声明动态特性使用范围的程序,无法构建成Native Image (例如,根据用户输入的一个参数反射去调用某个方法)。

    1. 在整个运行过程中,程序不会再加载任何新的class。

    => 在运行期间执行动态编译,或者是自定义Classloader动态装载类的程序,无法构建成Native Image。

    Native Image:环境安装

    GraalVM安装

    Native Image:实践

    介绍了Native Image的基本原理和限制后,让我们来实际实践看看这项技术到底能够带给我们什么。

    这里我们先给出一个非常基础的DEMO代码:

    public class HelloWorld {
    
        private static final String CONST = "this-is-a constant var";
    
        private String name;
    
        public HelloWorld(String name) {
            this.name = name;
        }
    
        public void sayHello() {
            System.out.println("hello, " + name);
        }
    
        public static void main(String[] args) {
            System.out.println(CONST);
            HelloWorld h1 = new HelloWorld("lumin");
            HelloWorld h2 = new HelloWorld(args[0]);
            h1.sayHello();
            h2.sayHello();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如何将这段代码构建成Native Image呢?首先安装好GraalVM,然后先使用javac将代码编译成字节码:

    $ javac HelloWorld.java
    
    • 1

    接下来执行Native Image Build,指定类名

    $ native-image HelloWorld
    
    • 1

    整个构建过程会执行比较长的一段时间,主要是执行Points-Analysis过程较长(大约三分多钟),最终的产物就是一个二进制文件:

    可以看到这个HelloWorld最终打包产出的二进制文件大小为8.2M,这是包含了SVM和JDK各种库后的大小,虽然相比C/C++的二进制文件来说体积偏大,但是对比完整JVM来说,可以说是已经是非常小了。

    再对比下运行速度:

    可以看到,相比于使用JVM运行,Native Image的速度要快上不少,cpu占用也更低一些,从官方提供的各类实验数据也可以看出Native Image对于启动速度和内存占用带来的提升是非常显著的:

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/552a1c94-4bb1-4d47-bb8b-1a58d3cc411e.png

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/ac072af7-48c0-4b44-b564-835a56080886.png

    接下来我们加上 -H:+PrintImageObjectTree -H:+ExhaustiveHeapScan -H:+PrintAnalysisCallTree的参数再进行一次build,这样可以将整个Points-to Analysis的详细过程(Object Tree和Call Tree)打印出来以供分析:

    call_tree_xxx文件中会包含完整的方法调用树,可以看到是一个递归的树形结构

    https://ata2-img.oss-cn-zhangjiakou.aliyuncs.com/neweditor/79455e41-c860-41e3-9e6b-262502c4a6cd.png

    通过Call Tree就可以得到整个程序运行过程中所有可能用到的方法,这些方法的代码都会被编译为机器码。

    object_tree_xxx文件中,则包含了代码中所有使用到的对象和变量:

    这里存储的主要是各种静态对象和变量,它们最终都被被打包至Image Heap中。

    最后我们再来看一个使用反射的例子:

    public class HelloReflection {
    
        public static void foo() {
            System.out.println("Running foo");
        }
    
        public static void bar() {
            System.out.println("Running bar");
        }
    
        public static void main(String[] args) {
            for (String arg : args) {
                try {
                    HelloReflection.class.getMethod(arg).invoke(null);
                }
                catch (ReflectiveOperationException ex) {
                    System.out.println("Exception running" + arg + ": "+ ex.getClass ().getSimpleName());
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这段代码接收用户的输入作为入参,然后通过反射调用用户指定的方法,我们通过普通方式来编译执行这段代码,是可以正常Work的:

    $ java HelloReflection foo bar
    Running foo
    Running bar
    
    
    • 1
    • 2
    • 3
    • 4
    d:\test>native-image HelloReflection
    ========================================================================================================================
    GraalVM Native Image: Generating 'helloreflection' (executable)...
    ========================================================================================================================
    [1/7] Initializing...                                                                                    (5.9s @ 0.08GB)
     Version info: 'GraalVM 22.2.0 Java 11 CE'
     Java version info: '11.0.16+8-jvmci-22.2-b06'
     C compiler: cl.exe (microsoft, x64, 19.32.31332)
     Garbage collector: Serial GC
    [2/7] Performing analysis...  [*****]                                                                    (6.9s @ 1.05GB)
       2,695 (73.98%) of  3,643 classes reachable
       3,437 (53.28%) of  6,451 fields reachable
      12,173 (45.34%) of 26,851 methods reachable
          26 classes,     0 fields, and   267 methods registered for reflection
          62 classes,    53 fields, and    52 methods registered for JNI access
           1 native library: version
    [3/7] Building universe...                                                                               (1.0s @ 0.58GB)
    
    Warning: Reflection method java.lang.Class.getMethod invoked at HelloReflection.main(HelloReflection.java:14)
    Warning: Aborting stand-alone image build due to reflection use without configuration.
    Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
    ------------------------------------------------------------------------------------------------------------------------
                            0.7s (5.0% of total time) in 14 GCs | Peak RSS: 1.94GB | CPU load: 6.07
    ========================================================================================================================
    Failed generating 'helloreflection' after 14.1s.
    ========================================================================================================================
    GraalVM Native Image: Generating 'helloreflection' (executable)...
    ========================================================================================================================
    [1/7] Initializing...                                                                                    (5.7s @ 0.08GB)
     Version info: 'GraalVM 22.2.0 Java 11 CE'
     Java version info: '11.0.16+8-jvmci-22.2-b06'
     C compiler: cl.exe (microsoft, x64, 19.32.31332)
     Garbage collector: Serial GC
    [2/7] Performing analysis...  [*****]                                                                    (7.5s @ 0.32GB)
       2,807 (74.79%) of  3,753 classes reachable
       3,564 (53.47%) of  6,666 fields reachable
      12,667 (45.85%) of 27,625 methods reachable
          26 classes,     0 fields, and   272 methods registered for reflection
          62 classes,    53 fields, and    52 methods registered for JNI access
           1 native library: version
    [3/7] Building universe...                                                                               (1.4s @ 0.80GB)
    [4/7] Parsing methods...      [*]                                                                        (1.0s @ 1.53GB)
    [5/7] Inlining methods...     [***]                                                                      (0.8s @ 0.46GB)
    [6/7] Compiling methods...    [***]                                                                      (5.5s @ 1.06GB)
    [7/7] Creating image...                                                                                  (1.7s @ 1.43GB)
       4.45MB (38.22%) for code area:     7,449 compilation units
       6.95MB (59.66%) for image heap:   90,863 objects and 5 resources
     252.45KB ( 2.12%) for other data
      11.64MB in total
    ------------------------------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                               Top 10 object types in image heap:
     664.30KB java.util                                          928.95KB byte[] for code metadata
     360.01KB java.lang                                          853.94KB java.lang.String
     353.50KB com.oracle.svm.jni                                 840.00KB byte[] for general heap data
     225.12KB java.util.regex                                    637.97KB java.lang.Class
     222.22KB java.text                                          526.25KB byte[] for java.lang.String
     207.03KB java.util.concurrent                               389.16KB java.util.HashMap$Node
     131.93KB com.oracle.svm.core.code                           352.09KB char[]
     117.02KB java.math                                          241.23KB com.oracle.svm.core.hub.DynamicHubCompanion
     110.77KB com.oracle.svm.core.genscavenge                    191.59KB java.util.HashMap$Node[]
      99.46KB sun.text.normalizer                                163.05KB java.lang.String[]
       1.96MB for 109 more packages                                1.41MB for 777 more object types
    ------------------------------------------------------------------------------------------------------------------------
                            0.9s (3.4% of total time) in 17 GCs | Peak RSS: 3.19GB | CPU load: 7.38
    ------------------------------------------------------------------------------------------------------------------------
    Produced artifacts:
     D:\test\helloreflection.build_artifacts.txt (txt)
     D:\test\helloreflection.exe (executable)
    ========================================================================================================================
    Finished generating 'helloreflection' in 25.0s.
    Warning: Image 'helloreflection' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary).
    
    d:\test>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    但是如果我们通过native-image运行,则会出现问题,这里我们需要加上–no-fallback参数来构建,否则graalvm检测到这个程序使用了未配置的反射时,会把产物自动降级成jvm运行:

    $ ./helloreflection foo
    Exception runningfoo: NoSuchMethodException
    
    • 1
    • 2

    可以看到,运行foo方法提示 NoSuchMethodException,这就是因为在编译时我们无法知道用户真正调用的会是哪个方法,因此静态编译的时候就不会把foo、bar这两个方法认为是“可达的”,最终的native image中也就不会包括这两个方法的机器码 。要解决这个问题,我们就需要进行 配置化的 提前声明

    在编译的目录下新建一个reflect-config.json,格式内容如下:

    [
      {
        "name": "HelloReflection",
        "methods": [{"name":"foo", "parameterTypes": []}]
      }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这样就相当于显示地声明了HelloReflection的foo方法会被反射调用,native build的时候就会将这个方法编译为机器码并写入image当中。可以再看下运行效果:

    编译日志

    [7/7] Creating image...                                                                                  (1.6s @ 0.96GB)
       4.28MB (37.44%) for code area:     7,124 compilation units
       6.91MB (60.45%) for image heap:   89,515 objects and 5 resources
     246.55KB ( 2.11%) for other data
      11.44MB in total
    ------------------------------------------------------------------------------------------------------------------------
    Top 10 packages in code area:                               Top 10 object types in image heap:
     635.07KB java.util                                          892.45KB byte[] for code metadata
     353.50KB com.oracle.svm.jni                                 840.44KB java.lang.String
     324.45KB java.lang                                          831.75KB byte[] for general heap data
     225.12KB java.util.regex                                    588.85KB java.lang.Class
     222.22KB java.text                                          516.40KB byte[] for java.lang.String
     166.94KB java.util.concurrent                               389.16KB java.util.HashMap$Node
     131.93KB com.oracle.svm.core.code                           352.09KB char[]
     117.02KB java.math                                          231.60KB com.oracle.svm.core.hub.DynamicHubCompanion
     110.77KB com.oracle.svm.core.genscavenge                    191.59KB java.util.HashMap$Node[]
      99.46KB sun.text.normalizer                                160.39KB java.lang.String[]
       1.90MB for 110 more packages                                1.39MB for 750 more object types
    ------------------------------------------------------------------------------------------------------------------------
                            0.8s (3.4% of total time) in 17 GCs | Peak RSS: 3.24GB | CPU load: 7.12
    ------------------------------------------------------------------------------------------------------------------------
    Produced artifacts:
     d:\test\helloreflection.build_artifacts.txt (txt)
     d:\test\helloreflection.exe (executable)
    ========================================================================================================================
    Finished generating 'helloreflection' in 23.2s.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行效果

    # linux
    $native-image -H:ReflectionConfigurationFiles=./reflect-config.json HelloReflection
    
    $./helloreflection foo
    Running foo
    
    $./helloreflection bar
    Exception runningbar: NoSuchMethodException
    
    # win10
    
    d:\test>helloreflection.exe
    
    d:\test>helloreflection.exe foo
    Running foo
    
    d:\test>helloreflection.exe foo bar
    Running foo
    Exception runningbar: NoSuchMethodException
    
    d:\test>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    可以看到,显示声明了的foo方法可以正常被调用,但是没有声明过的bar方法,依然会抛出NoSuchMethodException。

    Spring Native

    上面我们的实践都是比较简单的针对某一个Java Class而言,而我们实际线上使用的工程往往都要复杂许多,尽管Native Image也提供了编译一个完整jar包的能力,但是对于我们通常使用的spring、maven工程来说,由于反射和代理的存在,根本不可能直接通过Native Image编译成功,因此我们还需要工程框架层面的支持,否则Native Image永远无法成为一种生产工具,而更像一个玩具。

    作为Java工程界的龙头大佬,Spring自然观察到了这一点,于是就有了Spring Native。

    首先需要说明一下,Spring Native目前还属于实验特性,最新Beta版本为0.12.1,还没有推出稳定的1.0版本(按照官方预期是2022年内会推出),需要Spring Boot最低版本是2.6.6,后续Spring Boot 3.0中也会默认支持Native Image。

    https://github.com/spring-projects-experimental/spring-native

    在这里插入图片描述

    可以看到活跃度还是不错的,现在处于适配和扩展的阶段。

    那么,Spring Native给我们带来了什么呢?

    首先是Spring框架的Native化支持,包括IOC、AOP等各种Spring组件及能力的Native支持;其次是Configuration支持,允许通过@NativeHint注解来动态生成Native Image Configuration(reflect-config.json, proxy-config.json等);最后就是Maven Plugin,可以通过Maven构建获得Native Image,而不需要再手动去执行native-image命令。

    可以参考这篇Spring Native在Windows环境使用说明

    手动支持

    思路就是先打成jar包,然后native-image -cp spring-native-example-0.0.1-SNAPSHOT.jar
    生成二进制文件

    工程支持

    接下来我们通过一个DEMO来简单入门Spring Native

    首先确保Spring Boot的版本在2.6.6以上,然后在一个基础Spring Boot项目的基础上,引入以下依赖:

    
    <dependency>
        <groupId>org.springframework.experimentalgroupId>
        <artifactId>spring-nativeartifactId>
        <version>0.11.4version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接着引入plugin

    <plugin>
        <groupId>org.springframework.experimentalgroupId>
        <artifactId>spring-aot-maven-pluginartifactId>
        <version>0.11.4version>
        <executions>
            <execution>
                <id>generateid>
                <goals>
                    <goal>generategoal>
                goals>
            execution>
            <execution>
                <id>test-generateid>
                <goals>
                    <goal>test-generategoal>
                goals>
            execution>
        executions>
    plugin>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    最后指定native build的profile

    <profiles>
        <profile>
            <id>nativeid>
            <dependencies>
                
                <dependency>
                    <groupId>org.junit.platformgroupId>
                    <artifactId>junit-platform-launcherartifactId>
                    <scope>testscope>
                dependency>
            dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.buildtoolsgroupId>
                        <artifactId>native-maven-pluginartifactId>
                        <version>0.9.11version>
                        <extensions>trueextensions>
                        <executions>
                            <execution>
                                <id>build-nativeid>
                                <goals>
                                    <goal>buildgoal>
                                goals>
                                <phase>packagephase>
                            execution>
                            <execution>
                                <id>test-nativeid>
                                <goals>
                                    <goal>testgoal>
                                goals>
                                <phase>testphase>
                            execution>
                        executions>
                        <configuration>
                            
                        configuration>
                    plugin>
                    
                    <plugin>
                        <groupId>org.springframework.bootgroupId>
                        <artifactId>spring-boot-maven-pluginartifactId>
                        <configuration>
                            <classifier>execclassifier>
                        configuration>
                    plugin>
                plugins>
            build>
        profile>
    profiles>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    引入之后,运行mvn -Pnative -DskipTests clean package命令,就可以进入native build过程,编译完成后产物在 target/{your-app-name}。

    可以看下启动运行的效果:

    相比于普通JVM方式运行,启动速度大约提升了5倍(1.2s -> 0.2s)。

    对于大部分简单的Spring Boot应用,只需要经过上述这些简单的配置就可以完整运行了,看起来似乎很美好,是不是?但这仅仅是对于Spring组件而言,Native Image目前需要面对的最大问题,还是来自于Java世界数以万计的各种库:Netty、fastjson、logback、junit … 尽管很多的开源库都开始改造以支持Native Build,但对于生产环境的企业级应用来说,依然还有很长的路要走(当然,这可能也不是Native Image最适用的场景)。

    小结

    最后对Java的AOT方案做一个总结。Java AOT在经过一波三折的发展后,目前最为成熟可行的方案就是 GraalVM Native Image,它所带来的优势是显著的:更快的启动速度、更小的内存消耗、脱离JVM独立运行 。但对应的,它也存在着非常多的限制,尤其是在充满了反射等动态特性的Java工程生态圈,很难得到大规模的广泛应用。

    总的来说,Java AOT目前是有着明确使用场景的一种技术,主要可以应用于:

    1. 编写命令行CLI程序,希望程序可以完整独立运行而不是需要额外安装JVM。
    2. 运行环境资源严重受限的场景,例如IoT设备、边缘计算等场景。
    3. 希望追求极致的启动速度,并且应用逻辑相对足够轻量,如FaaS。

    当然未来Java AOT仍然会进一步发展,我们可以拭目以待。说不定能和go扳手腕就看这个

    附录

    《graal vm 22.1版本官方文档》

    《Spring Native官方文档》

  • 相关阅读:
    第九篇:– 过程发现(Process Discovery)是如何赋能数字化市场营销全过程?- 我为什么要翻译介绍美国人工智能科技巨头IAB公司
    vue.js:用户登录切换的小案例
    详细讲解FuzzBench如何添加新的Fuzzer
    C++ STL详解(五) -------- priority_queue
    zookeeper重启,线上微服务全部掉线,怎么回事?
    Python协程(asyncio)(二)高级开发
    具有独特底部轮廓的剥离光刻胶的开发
    猿创征文|阿里云MaxCompute存取性能测试报告
    03excel函数2
    微信小程序合集7(体育赛事+高仿知乎+微赞论坛+数独游戏+小熊日记)
  • 原文地址:https://blog.csdn.net/wdays83892469/article/details/126216765