前面松哥写了一篇文章和大家聊了 Spring6 中引入的新玩意 AOT(见Spring Boot3 新玩法,AOT 优化!)。
文章发出来之后,有小伙伴问松哥有没有做性能比较,老实说,这个给落下了,所以今天再来一篇文章,和小伙伴们梳理比较小当我们利用 Native Image 的时候,Spring Boot 启动性能从参数上来说,到底提升了多少。
先告诉大家结论:启动速度提升 10 倍以上。
1. Native Image
1.1 GraalVM
不知道小伙伴们有没有注意到,现在当我们新建一个 Spring Boot 工程的时候,再添加依赖的时候有一个 GraalVM Native Support
,这个就是指提供了 GraalVM 的支持。
那么什么是 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/):
下载后双击安装就行了,安装的时候选择 C++ 桌面应用开发。
如此之后,准备工作就算完成了。
3. 实践
接下来我们创建一个 Spring Boot 工程,并且引入如下两个依赖:
然后我们开发一个接口:
@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
打包完成之后,我们看下耗时时间:
耗时不算很久,差不多 3.7s 左右,算是比较快了,最终打成的 jar 包大小是 18.9MB。
再来看打成原生包,执行如下命令:
mvn clean native:compile -Pnative
这个打包时间就比较久了,需要耐心等待一会:
可以看到,总共耗时 4 分 54 秒。
Native Image 打包的时候,如果我们是在 Windows 上,会自动打包成 exe 文件,如果是 Mac/Linux,则生成对应系统的可执行文件。
这里生成的 aot_demo.exe 文件大小是 82MB。
两种不同的打包方式,所耗费的时间完全不在一个量级。
再来看启动时间。
先看 jar 包启动时间:
耗时约 1.326s。
再来看 exe 文件的启动时间:
好家伙,只有 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 源码分析的小伙伴,这块的代码应该都很好明白,这就是直接把 BeanDefinition 给解析出来了,不仅注册了当前 Bean,也把当前 Bean 所需要的依赖给注入了,将来 Spring 执行的时候就不用再去解析 BeanDefinition 了。
同时我们可以看到在 META-INF 中生成了 reflect、resource 等配置文件。这些是我们添加的 native-maven-plugin 插件所分析出来的反射以及资源等信息,也是 Spring AOT Processing 这个环节处理的结果。