Spring 6和SpringBoot中的提前优化AOT | baeldung


Spring 6 带来了一项有望优化应用程序性能的新功能:提前 (AOT) 编译支持。
在本文中,我们将探讨 Spring 6 的 AOT 优化功能的工作原理、它的好处以及如何使用它。

对于使用最多的 Java 虚拟机(JVM),如 Oracle 的 HotSpot JVM 和 OpenJDK,当我们编译源代码(.java 文件)时,生成的字节码存储在 .class 文件中。这样,JVM 使用 JIT 编译器将字节码转换为机器码。
此外,JIT 编译不仅涉及 JVM 对字节码的解释,还涉及在运行时将频繁执行的代码动态编译为本地机器码。

提前 (AOT) 编译是一种在应用程序运行之前将字节码预编译为本机机器代码的技术。
Java 虚拟机通常不支持此功能。然而,甲骨文在 OpenJDK 项目中为 HotSpot JVM 发布了一个名为“GraalVM Native Image”的实验性 AOT 特性,允许提前编译。
代码预编译后,计算机的处理器可以直接执行,省去了JVM解释字节码的麻烦,提高了启动时间。

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

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

让我们选择第二种选择,它是 Spring 6 的全新特性(第一种是传统构建,第二种是原生镜像)。

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

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

首先,我们需要搭建好AOT编译的环境。
我们可以设置构建插件来默认启用 AOT 编译:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <springAot>true</springAot>
        </systemPropertyVariables>
    </configuration>
</plugin>

其次,让我们使用以下命令构建具有 AOT 优化的应用程序:

mvn clean compile spring-boot:process-aot package

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

java -DspringAot=true -jar <jar-name>


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

  • 反射:它允许代码在编译时动态调用方法和访问未知的字段。AOT 编译器无法确定动态调用的类和方法。
  • 属性文件:属性文件的内容可以在运行时更改。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.json、resource-config.json等。
如果在此文件中定义了某些内容,则需要定义RuntimeHints,以便正确编译可执行文件。

可以在 GitHub 上找到示例的完整源代码。