Spring Boot3,启动时间缩短 10 倍!

前面松哥写了一篇文章和大家聊了 Spring6 中引入的新玩意 AOT(见Spring Boot3 新玩法,AOT 优化!)。

文章发出来之后,有小伙伴问松哥有没有做性能比较,老实说,这个给落下了,所以今天再来一篇文章,和小伙伴们梳理比较小当我们利用 Native Image 的时候,Spring Boot 启动性能从参数上来说,到底提升了多少。

先告诉大家结论:启动速度提升 10 倍以上。

1. Native Image

1.1 GraalVM

不知道小伙伴们有没有注意到,现在当我们新建一个 Spring Boot 工程的时候,再添加依赖的时候有一个 GraalVM Native Support,这个就是指提供了 GraalVM 的支持。

Spring Boot3,启动时间缩短 10 倍!_第1张图片

那么什么是 GraalVM 呢?

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

在运行上,GraalVM 同时支持 JIT 和 AOT 两种模式:

  • JIT 是即时编译(Just-In-Time Compilation)的缩写。它是一种在程序运行时将代码动态编译成机器码的技术。与传统的静态编译(Ahead-of-Time Compilation)不同,静态编译是在程序执行之前将代码编译成机器码,而 JIT 编译器在程序运行时根据需要将代码片段编译成机器码,然后再运行。所以 JIT 的启动会比较慢,因为编译需要占用运行时资源。我们平时使用 Oracle 提供的 Hotspot JVM 就属于这种。

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

如果我们在 Java 应用程序中使用了 AOT 技术,那么我们的 Java 项目就会被直接编译为机器码可以脱离 JVM 运行,运行效率也会得到很大的提升。

那么什么又是 Native Image 呢?

1.2 Native Image

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

Native Image 具备以下特点:

  • 即时启动:由于不需要 JVM 启动和类加载过程,Native Image 可以实现快速启动和即时执行。
  • 减少内存占用:编译成本地代码后,应用程序通常会有更低的运行时内存占用,因为它们不需要 JVM 的额外内存开销。
  • 静态分析:在构建 Native Image 时,GraalVM 使用静态分析来确定应用程序的哪些部分是必需的,并且只包含这些部分,这有助于减小最终可执行文件的大小。
  • 即时性能:虽然 JVM 可以通过JIT(Just-In-Time)编译在运行时优化代码,但 Native Image 提供了即时的、预先优化的性能,这对于需要快速响应的应用程序特别有用。
  • 跨平台兼容性:Native Image 可以为不同的操作系统构建特定的可执行文件,包括 Linux、macOS 和 Windows,即在 Mac 和 Linux 上自动生成系统可以执行的二进制文件,在 Windows 上则自动生成 exe 文件。
  • 安全性:由于 Native Image 不依赖于 JVM,因此减少了 JVM 可能存在的安全漏洞的攻击面。
  • 与 C 语言互操作:Native Image 可以与本地 C 语言库更容易地集成,因为它们都是在同一环境中运行的本地代码。

根据前面的介绍大家也能看到,GraalVM 所做的事情就是在程序运行之前,该编译的就编译好,这样当程序跑起来的时候,运行效率就会高,而这一切,就是利用 AOT 来实现的。

但是!对于一些涉及到动态访问的东西,GraalVM 似乎就有点力不从心了,原因很简单,GraalVM 在编译构建期间,会以 main 函数为入口,对我们的代码进行静态分析,静态分析的时候,一些无法触达的代码会被移除,而一些动态调用行为,例如反射、动态代理、动态属性、序列化、类延迟加载等,这些都需要程序真正跑起来才知道结果,这些就无法在编译构建期间被识别出来。

而反射、动态代理、序列化等恰恰是我们 Java 日常开发中最最重要的东西,不可能我们为了 Native Image 舍弃这些东西!因此,从 Spring6(Spring Boot3)开始支持 AOT Processing!AOT Processing 用来完成自动化的 Metadata 采集,这个采集主要就是解决反射、动态代理、动态属性、条件注解动态计算等问题,在编译构建期间自动采集相关的元数据信息并生成配置文件,然后将 Metadata 提供给 AOT 编译器使用。

道理搞明白之后,接下来通过一个案例来感受下 Native Image 的威力吧!

2. 准备工作

首先需要我们安装 GraalVM。

GraalVM 下载地址:

  • https://www.graalvm.org/downloads/

下载下来之后就是一个压缩文件,解压,然后配置一下环境变量就可以了,这个默认大家都会,我就不多说了。

GraalVM 配置好之后,还需要安装 Native Image 工具,命令如下:

gu install native-image

装好之后,可以通过如下命令检查安装结果:

另一方面,Native Image 在进行打包的时候,会用到一些 C/C++ 相关的工具,所以还需要在电脑上安装 Visual Studio 2022,这个我们安装社区版就行了(https://visualstudio.microsoft.com/zh-hans/downloads/):

Spring Boot3,启动时间缩短 10 倍!_第2张图片

下载后双击安装就行了,安装的时候选择 C++ 桌面应用开发。

Spring Boot3,启动时间缩短 10 倍!_第3张图片

如此之后,准备工作就算完成了。

3. 实践

接下来我们创建一个 Spring Boot 工程,并且引入如下两个依赖:

Spring Boot3,启动时间缩短 10 倍!_第4张图片

然后我们开发一个接口:

@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";
    }
}

这是一个很简单的接口,接下来我们分别打包成传统的 jar 和 Native Image。

传统 jar 包就不用我多说了,大家执行 mvn package 即可:

mvn package

打包完成之后,我们看下耗时时间:

Spring Boot3,启动时间缩短 10 倍!_第5张图片

耗时不算很久,差不多 3.7s 左右,算是比较快了,最终打成的 jar 包大小是 18.9MB。

再来看打成原生包,执行如下命令:

mvn clean native:compile -Pnative

这个打包时间就比较久了,需要耐心等待一会:

Spring Boot3,启动时间缩短 10 倍!_第6张图片

可以看到,总共耗时 4 分 54 秒。

Native Image 打包的时候,如果我们是在 Windows 上,会自动打包成 exe 文件,如果是 Mac/Linux,则生成对应系统的可执行文件。

Spring Boot3,启动时间缩短 10 倍!_第7张图片

这里生成的 aot_demo.exe 文件大小是 82MB。

两种不同的打包方式,所耗费的时间完全不在一个量级。

再来看启动时间。

先看 jar 包启动时间:

Spring Boot3,启动时间缩短 10 倍!_第8张图片

耗时约 1.326s。

再来看 exe 文件的启动时间:

Spring Boot3,启动时间缩短 10 倍!_第9张图片

好家伙,只有 0.079s。

1.326/0.079=16.78

启动效率提升了 16.78 倍!

我画个表格对比一下这两种打包方式:

jar Native Image
包大小 18.9MB 82MB
编译时间 3.7s 4分54s
启动时间 1.326s 0.079s

从这张表格中我们可以看到,Native Image 在打包的时候比较费时间,但是一旦打包成功,项目运行效率是非常高的。Native Image 很好的解决了 Java 冷启动耗时长、Java 应用需要预热等问题。

最后大家可以自行查看打包成 Native Image 时候的编译结果,如下图:

Spring Boot3,启动时间缩短 10 倍!_第10张图片
Spring Boot3,启动时间缩短 10 倍!_第11张图片
Spring Boot3,启动时间缩短 10 倍!_第12张图片

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

同时我们可以看到在 META-INF 中生成了 reflect、resource 等配置文件。这些是我们添加的 native-maven-plugin 插件所分析出来的反射以及资源等信息,也是 Spring AOT Processing 这个环节处理的结果。

你可能感兴趣的:(关于Spring,Boot,spring,boot,后端,java)