Spring Boot3 新玩法,AOT 优化!

今天和小伙伴们来聊一聊 Spring6 中的一个新特性 AOT(Ahead of Time),这也是目前在学习 Spring6 源码视频的小伙伴的一个提问,其实还是挺有代表意义的,因此松哥整理一篇文章来和大家聊一聊这个话题。

1. JIT VS AOT

1.1 JIT

JIT 是即时编译(Just-In-Time Compilation)的缩写。它是一种在程序运行时将代码动态编译成机器码的技术。与传统的静态编译(Ahead-of-Time Compilation)不同,静态编译是在程序执行之前将代码编译成机器码。

JIT 编译器在程序运行时根据需要将代码片段编译成机器码,以提高程序的执行效率。JIT 编译器通常用于解释型语言或动态语言的执行环境中,可以在运行时将解释的代码转换为机器码,从而提高程序的执行速度。

所以 JIT 启动比较慢,因为编译需要占用运行时的资源。

1.2 AOT

AOT 是预先编译(Ahead-of-Time Compilation)的缩写。它是一种在程序执行之前将代码静态编译成机器码的技术。与即时编译(JIT)不同,即时编译是在程序运行时动态地将代码编译成机器码。AOT 编译器在程序构建或安装阶段将代码转换为机器码,然后在运行时直接执行机器码,而无需再进行编译过程。这种静态编译的方式可以提高程序的启动速度和执行效率,但也会增加构建和安装的时间和复杂性。AOT 编译器通常用于静态语言的编译过程,如 C、C++ 等。

在 Spring 中应用 AOT 需要关注以下注意事项:

  1. 类路径是固定的,并且在构建的时候定义好。
  2. 程序中定义的 Bean 不能在运行的时候修改,这意味着:
    1. @Profile 注解标记的环境需要在构建的时候就确定。
    2. 条件注解 @Conditional 中的限制条件仅在构建时候考虑。
  3. 通过 supplier 提供的 Bean 无法提前进行类型转换(这种要在 Bean 创建的时候根据 supplier 去提供 Bean,具体可参考 Spring 源码视频)。
  4. 确保 Bean 的类型足够精确。

2. AOT 工作流程

Spring Framework 6 引入了AOT(Ahead-Of-Time)编译的概念,这是一种提前编译 Spring 应用程序的技术,以优化运行时性能,减少启动时间,并为创建 GraalVM 原生镜像提供支持。

AOT 的工作原理是在应用程序打包过程中提前执行那些通常在运行时进行的操作。包括生成 Bean 定义、解析配置和处理依赖注入等。通过这种方式,Spring 应用程序可以在启动时跳过这些步骤,从而加快启动速度,并减少 JVM 在运行时的计算负担。

AOT 的使用通常涉及以下几个步骤:

  1. 使用 ApplicationContextAotGenerator:这是 AOT 引擎的入口点,它负责处理 ApplicationContext 的配置。它会创建 Bean 定义,但不会实例化 Bean。
  2. 刷新 ApplicationContext:为了 AOT 处理,需要刷新 ApplicationContext,但这个刷新过程与传统的不同,因为它不会创建 Bean 实例。
  3. 调用 BeanFactoryInitializationAotProcessor 实现:这些处理器会对 GenerationContext 进行操作,例如,生成代码来恢复 BeanFactory 的状态。
  4. 更新 GenerationContext:完成上述步骤后,GenerationContext 会包含生成的代码、资源和类,这些都是应用程序运行所必需的。
  5. 生成 GraalVM 原生镜像配置:使用 RuntimeHints 实例生成 GraalVM 原生镜像配置文件。
  6. 获取 ApplicationContextInitializer 的类名:ApplicationContextAotGenerator#processAheadOfTime 会返回一个类名,这个类允许应用程序以 AOT 优化的方式启动。

乍一看,AOT 不错呀,还等什么,赶紧用 AOT 来跑我的项目吧!

别急!首先大家看到了 AOT 的有点,但是,这些优点中也隐藏着一些问题:

  • 反射:反射允许代码在编译时动态调用方法和访问未知的字段。AOT 编译器无法确定动态调用的类和方法。
  • 属性文件:属性文件的内容可以在运行时更改。由于作用时机的问题,AOT 编译器无法确定动态使用的属性文件。
  • 代理:代理可将方法调用动态重定向到其他对象,所以它会使 AOT 编译器难以确定在运行时调用哪些类和方法。
  • 序列化:序列化将对象的状态转换为字节流,反之亦然,这会使 AOT 编译器难以确定将在运行时调用哪些类和方法。

不过对于这些问题其实也都有办法处理,这就是 AOT 预处理了,这个咱们后文说。

3. 实践

接下来我们就来通过一个案例体验下 AOT 具体应用吧。

3.1 准备工作

Java 虚拟机通常是 JIT 形式,如果我们想要体验 AOT,那么就需要一个既支持 JIT 又支持 AOT 的工具了,这就是 GraalVM。

GraalVM 是一种高性能的通用虚拟机,它为 Java 应用提供 AOT 编译和二进制打包能力,基于 GraalVM 打出的二进制包可以实现快速启动、具有超高性能、无需预热时间、同时需要非常少的资源消耗。

GraalVM 非常有特色的一个功能是提供了 Native Image 打包技术,这种打包方式可以将应用程序打包为一个可脱离 JVM 独立运行的二进制包,这样就省去了 JVM 加载和字节码运行期预热的时间,提升了程序的运行效率。

和我们常用的 HotSpot JVM 相比主要有如下区别:

  1. 编译器技术:HotSpot JVM 使用传统的即时编译器(JIT)技术,将字节码实时编译为本地机器码。而 GraalVM 使用了一种新的即时编译器技术,称为 Graal 编译器。Graal 编译器采用了基于图形的优化方法,可以更好地优化代码并提高执行性能。
  2. 多语言支持:HotSpot JVM 主要为 Java 语言提供运行时环境,而 GraalVM 支持多种编程语言,包括 Java、JavaScript、Python、Ruby 等。这使得 GraalVM 成为一个更加通用和灵活的虚拟机。
  3. 内存占用:GraalVM 在内存占用方面相对较低,这是由于其编译器技术和优化策略的改进。相比之下,HotSpot JVM 在某些情况下可能会占用更多的内存。
  4. 生态系统集成:HotSpot JVM 是 Java 开发生态系统中广泛使用的虚拟机,有大量的工具和框架与其集成。GraalVM 也可以与现有的 Java 生态系统集成,但由于其多语言支持和特殊的编译器技术,可能需要一些额外的配置和适配。

当然,更重要的是,GraalVM 既支持 JIT 又支持 AOT。

所以,我们需要首先下载并安装 GraalVM。

下载地址:https://www.graalvm.org/downloads/,大家下载和自己 JDK 版本对应的 GraalVM。

这个下载之后直接解压就可以了,解压之后,将 GraalVM 配置到环境变量中就可以了。

最后,还需要安装一下 native-image,当然大家可以顺便用这个安装检验一下自己的 GraalVM 是否配置正确:

3.2 代码实践

接下来我们创建一个 Spring Boot 工程,来体验一下 AOT 提前编译。

首先在创建工程的时候我们多添加一个依赖 GraalVM Native Support,如下图:

这是一个用来支持 AOT 的插件。

代码创建好之后,我们随便开发一个 /hello 接口,然后就来给项目打包。

3.2.1 传统打包

直接点击 package 进行打包:

打包结果:

这个就是我们传统的打包方式,没啥好说的。大家注意一下这种传统打包方式打包的时间是 4.86s。

3.2.2 native image 打包

接下来我们来看下 native image 打包。

执行如下命令进行 native image 打包:

1
mvn clean native:compile -Pnative

打包结果如下图:

大家看这个构建时间超级长。

再来看 native image 构建的结果:

大家看到,除了我们所熟悉的 xxx.jar,还有一个可执行文件。

因为我这里是 Mac,所以打包出来的可执行文件没有后缀,如果在 Windows 上测试的话,打包出来的就是 aot_demo.exe 了。

现在这两个都可以直接运行。

jar 包就不用说了,大家都比较熟悉了。aot_demo 这个文件则是一个可以脱离 JVM 直接运行的二进制文件,启动效率会高很多。

根据第二小节的介绍,我们知道在打成原生包的时候,Spring AOT 会先进行 AOT 预处理,这个处理过程会创建 Bean 的定义,但是不会实例化 Bean,我们可以分析一下编译的结果就知道了。

首先我的源代码,除了启动类有两个类,分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {

@Autowired
HelloService helloService;

@GetMapping("/hello")
public String hello() {
return helloService.sayHello();
}
}
@Service
public class HelloService {
public String sayHello() {
return "hello aot";
}
}

在打 native-image 的时候,我们看下结果:



看过松哥之前将的 Spring 源码分析的小伙伴,这块的代码应该都很好明白,这就是直接把 BeanDefinition 给解析出来了,不仅注册了当前 Bean,也把当前 Bean 所需要的依赖给注入了,将来 Spring 执行的时候就不用再去解析 BeanDefinition 了。

同时我们可以看到在 META-INF 中生成了 reflect、resource 等配置文件。这些是我们添加的 native-maven-plugin 插件所分析出来的反射以及资源信息,将自动将这些作为配置文件生成的。

这块其实能聊的还蛮多,而且作为一个新支持的特性,Spring 对其功能也在不断完善,松哥后面会继续跟大家捋一捋这块的内容。

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×