SpringBoot 3 进阶教程之二 GraalVM 与 AOT
大纲
- SpringBoot 3 进阶教程之一整合 Prometheus 与 Grafana
- SpringBoot 3 进阶教程之二 GraalVM 与 AOT
- SpringBoot 3 进阶教程之三整合 Spring Security
- SpringBoot 3 进阶教程之四自定义 Starter
前言
本文主要介绍 SpringBoot 3 如何使用 AOT 技术,包括在 Windows、Linux 平台使用 GraalVM 将 SpringBoot 应用编译成原生镜像(二进制可执行文件)。
AOT 与 JIT
AOT
:Ahead of Time(提前编译),程序执行前,全部被编译成机器码JIT
:Just in Time(即时编译),程序边编译边运行
编译器与解释器
编程语言的分类
- 编译型语言:依赖编译器 (Complier),如 C、C++
- 解释型语言:依赖解释器 (Interpreter),如 JavaScrpt、Python
对比项目 | 编译器 (Complier) | 解释器 (Interpreter) |
---|---|---|
机器执行速度 | 快,因为源代码只需被转换一次 | 慢,因为每行代码都需要被解释执行 |
开发效率 | 慢,因为需要耗费大量时间编译 | 快,无需花费时间生成目标代码,更快的开发和测试 |
调试 | 难以调试编译器生成的目标代码 | 容易调试源代码,因为解释器一行一行地执行 |
可移植性(跨平台) | 不同平台需要重新编译目标平台代码 | 同一份源码可以跨平台执行,因为每个平台会开发对应的解释器 |
学习难度 | 相对较高,需要了解源代码、编译器以及目标机器的知识 | 相对较低,无需了解机器的细节 |
错误检查 | 编译器可以在编译代码时检查错误 | 解释器只能在执行代码时检查错误 |
运行时增强 | 无 | 可以动态增强 |
AOT 与 JIT 对比
在 OpenJDK 的官方 Wiki 上,介绍了 HotSpot 虚拟机一个相对比较全面的、即时编译器(JIT)中采用的 优化技术列表。Java 应用可以使用 JVM 参数
-XX:+PrintCompilation
打印 JIT 编译信息。
JVM 编译原理
JVM 整体架构
JVM 既有解释器,又有编译器,因此可以说 Java 是半编译半解释的编程语言。
Java 执行流程
建议阅读
流程概要
详细流程
JVM 编译器
JVM 中集成了两种编译器,分别是 Client Compiler 和 Server Compiler
Client Compiler
:注重启动速度和局部的优化Server Compiler
:更加关注全局优化,性能更好,但由于会进行更多的全局分析,所以启动速度会慢
Client Compiler
- HotSpot 虚拟机带有一个 Client Compiler C1 编译器
- 这种编译器启动速度快,但是性能比较 Server Compiler 来说会差一些
- 编译后的机器码执行效率没有 C2 的高
Server Compiler
- Hotspot 虚拟机中使用的 Server Compiler 有两种: C2 和 Graal
- 在 Hotspot 虚拟机中,默认的 Server Compiler 是 C2 编译器
分层编译
在 Java 7 以前,需要研发人员根据服务的性质去选择编译器。对于需要快速启动的,或者一些不会长期运行的服务,可以采用编译效率较高的 C1,对应参数 -client
。长期运行的服务,或者对峰值性能有要求的后台服务,可以采用峰值性能更好的 C2,对应参数 -server
。Java 7 开始引入了分层编译的概念,它结合了 C1 和 C2 的优势,追求启动速度和峰值性能的一个平衡。分层编译将 JVM 的执行状态分为了五个层次。五个层级分别是:
- 解释执行
- 执行不带
profiling
的 C1 代码 - 执行仅带方法调用次数以及循环回边执行次数
profiling
的 C1 代码。 - 执行带所有
profiling
的 C1 代码 - 执行 C2 代码
- 图中第 ① 条路径,代表编译的一般情况,热点方法从解释执行到被 3 层的 C1 编译,最后被 4 层的 C2 编译。
- 如果方法比较小(比如 Java 服务中常见的
getter
与setter
方法),3 层的profiling
没有收集到有价值的数据,JVM 就会断定该方法对于 C1 代码和 C2 代码的执行效率相同,就会执行图中第 ② 条路径。在这种情况下,JVM 会在 3 层编译之后,放弃进入 C2 编译,直接选择用 1 层的 C1 编译运行。 - 在 C1 忙碌的情况下,执行图中第 ③ 条路径,在解释执行过程中对程序进行
profiling
,根据信息直接由第 4 层的 C2 编译。 - 由于 C1 中的执行效率是 1 层 > 2 层 > 3 层,第 3 层一般要比第 2 层慢 35% 以上,所以在 C2 忙碌的情况下,执行图中第 ④ 条路径。这时方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。
- 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第 ⑤ 条执行路径代表的就是反优化。
总的来说,C1 的编译速度更快,C2 的编译质量更高,分层编译的不同编译路径,也就是 JVM 根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从 JDK 8 开始,JVM 默认开启分层编译。
云原生介绍
在云原生 (Cloud Native) 的背景下,Java 应用的运行条件发生了变化。
存在的问题
- Java 应用如果用 Jar 启动,解释执行后热点代码才被编译成机器码,会导致初始启动速度慢,初始处理请求数量少。
- 大型云平台,要求每一种应用都必须秒级启动,每个应用都要求高效率。
希望的效果
- Java 应用也能提前被编译成机器码,随时急速启动,一启动就急速运行,追求最高性能
- 编译成机器码的优点
- 服务器不需要安装 Java 运行环境
- 应用编译成机器码后,可以在 Windows x64 等平台直接运行
原生镜像是什么?
- 原生镜像 (Native Image):机器码、本地镜像
- 把应用打包成能适配本机平台的可执行文件(机器码、本地镜像)
GraalVM 介绍
GraalVM 是一个高性能的 JDK,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行,同时还提供 JavaScript、Python 和许多其他流行语言的运行时。GraalVM 提供了两种运行 Java 应用程序的方式:
第一种运行方式
:在 HotSpot JVM 上使用 Graal 即时编译器(JIT)第二种运行方式
:作为预先编译(AOT)的本机可执行文件运行(原生镜像)
值得一提的是,GraalVM 的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言调用的成本。
整体架构
工作原理
GraalVM 跨平台提供原生镜像的原理如下图所示:
特别说明
目前并不是所有 Java 代码都支持直接使用 GraalVm 编译成原生镜像(二进制可执行文件),具体存在的兼容问题如下:
动态能力损失:
- 问题描述:GraalVM 不支持直接编译反射代码,包括动态获取构造器、反射创建对象、反射调用等
- 解决方案:额外使用 SpringBoot 提供的一些注解,提前告知 GraalVm 反射会用到哪些构造器、方法等
配置文件损失:
- 问题描述:以二进制可执行文件的方式运行 Java 应用,项目内原有的配置文件会失效
- 解决方案:额外处理(如使用配置中心、使用配置文件的相对路径等),提前告知 GraalVM 配置文件怎么处理
值得一提的是,SpringBoot 可以保证 Spring 应用都能在使用 AOT 特性的时候,提前告知 GraalVm 怎么处理,但并不是所有框架都适配了 AOT 特性,尤其是第三方框架。
Linux 平台 编译原生镜像
在 Linux 平台,使用 GraalVM 编译原生镜像,需要提前安装 GCC 和 GraalVM。
GCC 安装
安装 GCC/G++ 的目的是为了可以编译 C/C++ 代码。
1 | yum install -y gcc gcc-c++ glibc-devel kernel-devel zlib-devel |
Graalvm 安装
下载软件
根据开发平台和 JDK 版本,在 GraalVM GitHub Releases 页面上下载 GraalVM 与 Native Image 的软件包(如下图)。值得一提的是,GraalVM 有两种版本,分别是社区版和商业版,开发环境一般使用社区版即可。
配置环境
解压 GraalVM 的软件包(如 graalvm-ce-java17-linux-amd64-22.3.3.tar.gz
),然后添加或更改系统环境变量 JAVA_HOME
与 Path
,其中 JAVA_HOME
指向 GraalVM 的解压目录,PATH
则指向 GraalVM 的 bin
目录路径。
1 | # 解压文件 |
在命令行终端输入 java -version
命令,验证 JDK 环境是否为 GraalVM 提供的。
提示
上述配置 GraalVM 环境变量的方式,与平时配置 OpenJDK 或者 Oracle JDK 的环境变量并没有任何区别。
安装工具
安装 Native Image 工具,用于编译生成原生镜像,安装方式分为在线安装和离线安装两种,如下所示:
- 网络环境好,可以选择在线安装 Native Image
1 | gu install native-image |
- 网络环境不好,可以使用上面下载好的 Native Image Jar 包(如
native-image-installable-xxxx.jar
)进行离线安装
1 | gu install --file native-image-installable-svm-java17-linux-amd64-22.3.3.jar |
- 验证工具的安装结果
1 | native-image --help |
提示
更多关于 Native Image 的安装说明,可以参考 GraalVM 官方文档。
编译原生镜像
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-boot3-18
。
创建项目
创建普通 Maven 项目,编写 Main
主类
- 使用
mvn clean package
命令进行打包 - 使用
java -jar xxx.jar
命令确认 Jar 包是否可以执行 - 若 Jar 包不能正常执行,则需要通过 Maven 插件指定主类的全类名,如下所示:
1 | <build> |
编译镜像
在命令行终端使用 native-image
工具编译原生镜像(二进制可执行文件),如下所示:
- 第一种编译方式
1 | # 只编译某个类,该类必须有 main 方法,否则无法编译(下面的 classes 目录一般是在 Maven 项目编译生成的 target 目录下) |
- 第二种编译方式
1 | # 从主类开始,编译整个 Jar 包(下面的 Jar 包一般是在 Maven 项目编译生成的 target 目录下) |
Windows 平台 编译原生镜像
在 Windows 平台,使用 GraalVM 编译原生镜像,需要提前安装 GraalVM 和 Visual Studio。
GraalVM 安装
下载软件
根据开发平台和 JDK 版本,在 GraalVM GitHub Releases 页面上下载 GraalVM 与 Native Image 的软件包(如下图)。值得一提的是,GraalVM 有两种版本,分别是社区版和商业版,开发环境一般使用社区版即可。
配置环境
解压 GraalVM 的软件包(如 graalvm-ce-java17-windows-amd64-22.3.3.zip
),然后添加或更改系统环境变量 JAVA_HOME
与 Path
,其中 JAVA_HOME
指向 GraalVM 的解压目录,PATH
则指向 GraalVM 的 bin
目录路径。
在 CMD 窗口输入 java -version
命令,验证 JDK 环境是否为 GraalVM 提供的。
提示
上述配置 GraalVM 环境变量的方式,与平时配置 OpenJDK 或者 Oracle JDK 的环境变量并没有任何区别。
安装工具
安装 Native Image 工具,用于编译生成原生镜像,安装方式分为在线安装和离线安装两种,如下所示:
- 网络环境好,可以选择在线安装 Native Image
1 | gu install native-image |
- 网络环境不好,可以使用上面下载好的 Native Image Jar 包(如
native-image-installable-xxxx.jar
)进行离线安装
1 | gu install --file native-image-installable-svm-java17-windows-amd64-22.3.3.jar |
- 验证工具的安装结果
1 | native-image --help |
提示
更多关于 Native Image 的安装说明,可以参考 GraalVM 官方文档。
Visual Studio 安装
安装 Visual Studio 的目的是为了使用 VS 提供的工具链编译 C/C++ 代码。值得一提的是,可以使用 MinGW、Cygwin 等编译工具替代 Visual Studio。
安装步骤
在 Visual Studio 安装时,一般选择安装 Microsoft.VisualStudio.Workload.NativeDesktop
、Microsoft.VisualStudio.Workload.Universal
这两大组件即可,分别对应下图红框内的组件。
语言包必须选择 英语
,不能选择中文,否则在 GraalVM 编译原生镜像时,可能会出现各种莫名奇妙的问题。
安装步骤完成后,如果使用管理员身份可以正常运行 x64 Native Tools Command Prompt for VS 20xx
工具,则说明 Visual Studio 安装成功。
编译原生镜像
本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-boot3-18
。
创建项目
创建普通 Maven 项目,编写 Main
主类
- 使用
mvn clean package
命令进行打包 - 使用
java -jar xxx.jar
命令确认 Jar 包是否可以执行 - 若 Jar 包不能正常执行,则需要通过 Maven 插件指定主类的全类名,如下所示:
1 | <build> |
编译镜像
运行 Visual Studio 的 x64 Native Tools Command Prompt for VS 20xx
工具,使用 native-image
工具编译原生镜像(二进制可执行文件),如下所示:
- 第一种编译方式
1 | # 只编译某个类,该类必须有 main 方法,否则无法编译(下面的 classes 目录一般是在 Maven 项目编译生成的 target 目录下) |
- 第二种编译方式
1 | # 从主类开始,编译整个 Jar 包(下面的 Jar 包一般是在 Maven 项目编译生成的 target 目录下) |
SpringBoot 编译原生镜像
这里将演示 SpringBoot 项目如何使用 GraaVM 编译生成原生镜像,本章节完整的案例代码可以直接从 GitHub 下载对应章节 spring-boot3-19
。
特别注意
在执行以下步骤之前,必须确保在 Windows/Linux 平台配置好了 GraalVM 编译所需要的环境,包括 GraalVM 安装、Visual Studio 安装或者 GCC 安装等。
引入依赖
- 添加 GraalVM 编译插件和 SpringBoot 打包插件的依赖
1 | <build> |
编译镜像
- 第一步:运行 Maven 的打包命令
mvn clean package
- 第二步:运行 AOT 的提前处理命令
mvn spring-boot:process-aot
- 第三步:运行 Native Image 的编译命令
mvn -Pnative native:build
,其中的-Pnative
表示激活native
环境的 Profile 配置文件,建议都带上这参数进行编译
启动镜像
SpringBoot 应用成功编译原生镜像后,在项目的 target
目录下,可以看到编译生成的原生镜像(二进制可执行文件)。
在项目的 target
目录下,直接运行 ./spring-boot3-19.exe
就可以快速启动镜像,观察可以发现应用的整个启动过程仅仅耗费 0.137 秒。
1 | . ____ _ __ _ _ |
常见问题
Windows 平台
编译原生镜像失败
问题描述:GraalVM 编译生成原生镜像时,可能出现如下各种错误
- 出现乱码
- 出现
cl.exe
找不到错误 - 提示
no include path set
- 提示
fatal error LNK1104: cannot open file 'LIBCMT.lib'
- 提示
LINK : fatal error LNK1104: cannot open file 'kernel32.lib'
- 提示各种其他找不到的内容
解决方案:修改三个环境变量:
Path
、INCLUDE
、lib
,请自行根据 Visual Studio 的实际安装路径来更改环境变量的值
PATH
环境变量添加如下值:
1 | C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\bin\Hostx64\x64 |
- 新建
INCLUDE
环境变量,值为:
1 | C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt |
- 新建
lib
环境变量,值为:
1 | C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\lib\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64 |