Spring 6 中的 AOT(Ahead of Time)优化

1、概览

Spring 6 提供了一项新功能,有望优化应用程序的性能: Ahead-of-Time(AOT) 编译支持。

在本文中,我们将了解 Spring 6 中 AOT 优化功能的工作原理、优点以及使用方法。

2、AOT 编译

2.1、JIT 编译器

对于最常用的 Java 虚拟机(JVM),如 Oracle 的 HotSpot JVM 和 OpenJDK,当我们编译源代码(.java 文件)时,生成的字节码存储在 .class 文件中。这样,JVM 就会使用 JIT(Just-In-Time,即时编译) 编译器将字节码转换为机器码。

此外,JIT 编译涉及 JVM 对字节码的解释,以及在运行时将频繁执行的代码动态编译为本地机器码。

2.2、AOT 编译器

Ahead-of-Time(AOT,提前编译或预编译)是一种在应用程序运行前将字节码预编译为本地机器码的技术。

Java 虚拟机(JVM)通常不支持这一功能。不过,甲骨文已在 OpenJDK 项目中发布了一项针对 HotSpot JVM 的实验性 AOT 功能,名为 “GraalVM Native Image”,允许进行 AOT 编译。

预编译代码后,计算机处理器可直接执行代码,无需 JVM 解释字节码,从而缩短了启动时间。

关于 AOT 编译器的更多细节可以参考 官方文档

3、Spring 6 中的 AOT 支持

3.1、AOT 编译优化

在构建 Spring 6 应用程序时,我们需要考虑三种不同的运行时选项:

  • 运行在 JRE 上的传统 Spring 应用程序。
  • 在 AOT 编译阶段生成并在 JRE 上运行的代码。
  • 在 AOT 编译阶段生成并在 GraalVM 原生镜像中运行的代码。

让我们来看看第二种方案,它是 Spring 6 的全新功能(前者是传统构建,后者是原生镜像)。

首先,我们需要为 AOT 编译 设置好环境

通过 AOT 编译构建的应用程序在性能和资源消耗方面具有多重优势:

  • 消除死代码:AOT 编译器可以消除运行时从未执行过的代码。这样可以减少需要执行的代码量,从而提高性能。
  • 内联:内联是 AOT 编译器用函数的实际代码,替换函数调用的一种技术。这可以减少函数调用的开销,从而提高性能。
  • 常量传播:AOT 编译器通过在编译时确定变量的常量值来替换变量,从而优化性能。这样就无需进行运行时计算,从而提高了性能。
  • 过程间优化:AOT 编译器可通过分析程序的调用图来优化跨多个函数的代码。这可以通过减少函数调用的开销和识别常见的子表达式来提高性能。
  • Bean 定义:Spring 6 中的 AOT 编译器可减少不必要的 BeanDefinition 实例,从而提高应用程序的效率。

因此,让我们使用 AOT 优化命令来构建应用程序:

mvn clean compile spring-boot:process-aot package

然后使用命令运行应用程序:

java -Dspring.aot.enabled=true -jar <jar-name>

我们可以设置构建插件,默认启用 AOT 编译:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
    <execution>
        <id>process-aot</id>
        <goals>
        <goal>process-aot</goal>
        </goals>
    </execution>
    </executions>
</plugin>

3.2、AOT 优化中的问题

当我们决定使用 AOT 编译来构建应用程序时,可能会遇到一些问题,例如:

  • 反射:它允许代码动态调用编译时未知的方法和访问未知的字段。AOT 编译器无法确定动态调用的类和方法。
  • Properties 文件:properties 文件的内容可能在运行时发生变化。AOT 编译器无法确定动态使用的属性文件。
  • 代理:代理通过提供另一个对象的代理来控制对该对象的访问。由于代理可用于将方法调用动态重定向到其他对象,因此 AOT 编译器很难确定运行时将调用哪些类和方法。
  • 序列化:序列化将对象的状态转换为字节流,反之亦然。总的来说,这会使 AOT 编译器难以确定哪些类和方法将在运行时被调用。

为了确定哪些类会导致 Spring 应用程序出现问题,我们可以运行一个代理来提供有关反射操作的信息。

因此,让我们配置 Maven 插件,使其包含一个 JVM 参数,以协助实现这一点。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <jvmArguments>
            -agentlib:native-image-agent=config-output-dir=target/native-image
        </jvmArguments>
    </configuration>
    <!- ... -->
</plugin>

用如下命令运行:

./mvnw -DskipTests clean package spring-boot:run

target/native-image/ 中,我们可以找到生成的文件,如 reflect-config.jsonresource-config.json 等。

如果在该文件中定义了某些内容,就需要定义 RuntimeHints,以便正确编译可执行文件。

4、总结

在本文中,我们介绍了 Spring 6 中新的 AOT 优化功能。


参考:https://www.baeldung.com/spring-6-ahead-of-time-optimizations