JAVA:AOT入门

本文将介绍使用AOT相比使用JVM运行java程序的优点,以及如何使用AOT生成可执行文件。

资料来源:
GraalVM官网
阿里巴巴微信公众号

JIT

传统的一个 Java 应用从代码编写到启动运行大致可以分为如下步骤:

  1. 首先,编写.java源代码程序。
  2. 然后,借助javac工具将.java文件翻译为.class字节码。字节码是 Java 中非常重要的内容之一,正是因为它的出现,Java 才实现对底层环境的屏蔽,达到 Write once, run anywhere 的效果。
  3. 基于步骤2的.class文件会被打包成 jar 包或者 war 包进行部署执行,部署过程中通过Java虚拟机加载应用程序然后解释字节码运行业务逻辑。
    • 我们需要格外注意的是,.class->机器码的过程中,JVM类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了JIT(Just in Time Compilation)编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说Java 是编译与解释共存的语言。

示意图:

AOT

传统方案的缺陷


Java 程序启动运行详细过程如上图所示。

具体过程:

  1. 一个 Java 应用启动过程首先需要加载该应用程序对应的JVM 虚拟机软件程序到内存中,如上图红色部分描述所示。
  2. 然后 JVM 虚拟机再加载对应的应用程序到内存中,该过程对应上图中的浅蓝色类加载(Class Load,CL)部分。
  3. 在类加载过程中,应用程序就会开始被解释执行,对应上图中浅绿色部分。
  4. 解释执行过程 JVM 对垃圾对象进行回收,对应上图中的黄色部分。
  5. 随着程序的运行的深入,JVM 会采用及时编译(Just In Time,JIT)技术对执行频率较高的代码进行编译优化,以便提升应用程序运行速度。JIT 过程对应上图中的白色部分。
  6. 经过 JIT 编译优化后的代码对应图中深绿色部分。

经过上述分析,不难看出,一个 Java 程序从启动到达到被JIT动态编译优化会经过VM initApp initApp active几个阶段,相比于其他一些编译型语言,其冷启动问题比较严重。

除了冷启动问题,在一个 Java 程序运行过程中,什么都不做首先就需要加载一个 JVM 虚拟机,该操作一般占用一定内存。另外,由于 Java 程序是先解释执行字节码,然后再做 JIT 编译优化。由于相比于一些编译型语言其将编译优化的动作后置到运行时,因此非常容易出现实际加载的代码比实际需要运行的代码多很多的情况,造成了一些无效内存占用情况。综上所述就是为什么很多人常诟病 Java 程序运行内存占用高的几点主要原因。

解决方案:AOT

提前编译(Ahead-of-Time Compilation,AOT Compilation)或者叫静态编译在 Java 领域很早就被提了出来。其核心思想就是让 Java 程序也跟其他程序语言,比如 C/C++ 一样,先编译后执行解决上述问题,将 Java 程序的编译阶段提前到程序启动前,然后在编译阶段进行代码编译优化,让程序启动既巅峰,消除冷启动,降低运行时内存开销。

Java 领域静态编译的实现技术有很多,其中最具代表性的还属 Oracle 推出的 GraalVM 开源高性能多语言运行时平台。


GraalVM 中通过提供 Truffle 解释器实现框架,让开发人员可以使用 Truffle 提供的 API 快速实现特定语言的解释器从而实现对上图中各种编程语言所写的程序都能进行编译运行的效果,从而成为一个多语言运行时平台。GraalVM 实现静态编译能力的编译器就是 GraalVM JIT Compiler。静态编译框架和运行时由 Substrate VM 子项目实现,兼容 OpenJDK 运行时实现,提供了原生镜像程序运行时的异常处理、同步调度、线程管理、内存管理等功能。
因此,GraalVM 不仅可以作为一个多语言运行时平台,而且由于其中提供的 GraalVM JIT Compiler 静态编译器,其可用来对 Java 程序进行静态编译。

基于静态编译的 Java 程序相比于目前应用广泛的 JVM 运行时编译 Java 程序,整个从代码编写到编译执行的区别如下:

相比于 JVM 运行时方式,静态编译在运行之前会先对程序解析编译,然后生成一个跟运行时环境强相关的 native image 可执行文件,最后直接执行该文件即可启动程序进行执行。

静态编译过程

静态编译过程到底会对 Java 程序做哪些解析操作?静态编译后的可执行程序垃圾回收问题怎么解决?

GraalVM 静态编译实现示意图:

图中左侧前三个输入内容 Applicaton,Libraries 和 JDK 是一个 Java 程序编译运行必备的三部分,不必多说。而 Substrate VM 就是 GraalVM 中实现静态编译的核心部分,在整个静态编译过程中扮演了重要作用。
在静态分析过程中Substrate VM 通过上下文不敏感的指向分析(Points-to Analysis)来对应用程序做静态分析,其可以在不需要运行程序的情况下,基于源程序分析给出所有可能的可达函数列表然后作为后续编译阶段的输入对程序进行静态编译
在静态分析完成后,基于静态分析结果的可达函数列表,会调用介绍的 GraalVM 中的 GraalVM JIT Compiler 编译器将应用程序编译为与目标平台强相关的本地代码以完成编译过程。
编译完成后,就会进入到上图中右侧 Native 可执行文件生成阶段。在该过程中,Substrate VM 会将静态编译阶段确定和初始化的内容以及跟 Substrate VM 运行时以及 JDK 库中的数据一起保存到最终可执行文件的 Image Heap 中。其中 Substrate VM 运行时就为最终可执行文件提供了运行过程中所需的垃圾回收、异常处理等能力。

静态分析局限性

在Substrate VM的静态分析过程中,由于静态分析无法覆盖 Java 中的反射、动态代理、JNI 调用等动态特性。这也造成了很多的 Java 框架由于在实现过程中使用了大量的上述特性,因此,都难以直接基于 Substrate VM 完成对自身所有代码的静态分析,需要通过额外的外部配置来解决静态分析本身的不足。

一个java项目,该如何进行静态编译适配呢?其最核心要解决的本质问题,就是将开源框架中的 GraalVM 无法识别和处理的动态内容转换为其可识别的内容即可。

Spring 社区为了应对Spring项目中反射和动态代理等特性在静态编译环境下的挑战,开发了AOT Engine工具。AOT Engine专注于在构建阶段对Spring应用中的特定内容进行静态分析与转换处理,这些内容包括但不限于使用@Configuration注解声明的配置类及其初始化逻辑。
通过AOT Engine,能够识别并预先处理那些原本只能在运行时动态生成的类结构,如基于Java的动态代理类。这样,在静态编译阶段就能够有效生成这些原本动态创建的对象,确保它们能被Substrate VM或类似支持提前编译的技术识别和兼容。
因此,AOT Engine有效地解决了Spring应用在静态编译场景下可能遇到的问题,使得Spring应用能够适应静态编译并实现性能优化,同时保持框架的灵活性和功能完整性。

对于非 Spring 体系项目或者自身使用了一些 JDK 中原生的反射或者其他 Java 动态特性,针对自身代码中的 Java 动态用法需要在项目中提供对应的静态配置文件才能在静态编译过程中让编译器识别其中的动态特性,对其进行编译构建才能实现项目的顺利编译与执行。
针对这种情况,GraalVM 提供了一个名叫 native-image-agent 的 Tracing Agent 来帮助大家更方便地收集元数据并准备配置文件。该 Agent 会在常规 Java VM 上的应用程序运行过程中自动收集其中的动态特性使用情况并将其转换为 GraalVM 可以识别的配置文件。最后,将通过 Agent 生成的框架自身的动态配置文件存放在项目的:META-INF/native-image/<group.id>/<artifact.id> 目录下,就可以在静态编译过程中根据这些配置内容,识别项目包中的动态特性。

基于静态编译构建微服务

Spring Cloud Alibaba 2022.0.0.0 版本所包含的所有中间件客户端已完成了构建 GraalVM 原生应用的适配。为用户提供了开箱即用的静态编译能力。

环境准备

  1. 安装 GraalVM 发行版。(windows安装可参考:Using GraalVM and Native Image on Windows 10
  2. 检查 java -version 的输出来验证是否配置了正确的版本。

建议不要使用Windows。

应用构建

  1. 要使用 GraalVM 静态编译能力构建微服务,首先确保项目的Spring Boot版本为3.0.0或以上Spring Cloud版本为2022.0.0或以上。然后在项目中引入Spring Cloud Alibaba 2022.0.0.0版本的所需模块依赖即可。
  2. 通过maven命令生成应用中反射、序列化和动态代理所需的 Hints 配置文件:mvn -Pnative spring-boot:run(前提是应用中引入了spring-boot-starter-parent父模块)
  3. 之后应用会启动,进行预执行,需要尽可能完整的测试一遍应用的所有功能,保证应用的大部分代码都被测试用例覆盖,该过程会基于 GraalVM 的 native-image-agent 收集程序中的动态特性,这样可以确保完整生成应用运行过程中的所有必须的动态属性。
  4. 运行完所有测试用例后,我们发现resource/META-INF/native-image目录下会生成以下一些hints文件:
    • resource-config.json:应用中资源 hint 文件
    • reflect-config.json:应用中反射定义 hint 文件
    • serialization-config.json:应用中序列化内容 hint 文件
    • proxy-config.json:应用中 Java 代理相关内容 hint 文件
    • jni-config.json:应用中 Java Native Interface(JNI)内容 hint 文件

上述预执行过程主要为了扫描应用自身业务代码以及其他第三方包中的动态特性,以便后续静态编译过程能顺利进行,应用能正常启动。

如果没有使用动态特性,可直接进行下一步。

静态编译

通过maven命令来构建原生镜像:mvn -Pnative native:compile
成功执行后,可在/target目录看到生成的可执行文件。

执行可执行文件

进入/target,命令行输入./<可执行文件名称>,即可发现项目被光速启动了。

这是真TM快啊!


JAVA:AOT入门
http://shoumingchilun.github.io/2024/03/18/backend/language/java/JIT_AOT/
作者
寿命齿轮
发布于
2024年3月18日
许可协议