首页 / Spring Boot 入门教程 / GraalVM 原生镜像

Spring Boot 入门教程

GraalVM 原生镜像

本教程共 48 篇 · 第 44 篇 · 更新于 2026-08-13 · 约 5 分钟阅读

Spring BootGraalVM原生镜像AOT云原生性能优化部署

本节目标:理解原生镜像和 AOT 编译的原理,掌握 Spring Boot 构建原生镜像的方法,知道它的限制和适用场景。

JVM 应用的两大痛点

Java 应用跑在 JVM 上,启动要先加载类、解释执行、逐步 JIT 编译。一个 Spring Boot 应用冷启动 3 到 5 秒很正常,内存占用几百 MB 起步。

大多数场景无所谓。但云原生时代变了:函数计算按调用计费,容器要秒级扩容,内存就是成本。启动慢、吃内存成了痛点。

原生镜像:换一条路

GraalVM 的 Native Image 技术换个思路:构建时就把 Java 代码编译成机器码,产物是一个独立的可执行文件,运行时不需要 JVM。

对比 JVM 部署:

维度JVM 部署原生镜像
编译时机运行时 JIT构建时 AOT
启动时间秒级毫秒级(快 10 倍以上)
内存占用低(可省一半以上)
可执行文件需要 JVM独立文件,自带一切
构建时间慢(几分钟)

启动快、内存小,特别适合容器和无服务器场景。代价是构建复杂、限制多。

AOT 编译:从入口开始静态分析

原生镜像的编译流程叫 AOT(Ahead-of-Time,提前编译)。编译器从 main 方法开始,静态分析所有可达的代码,全部编译成机器码。

静态分析带来几个直接后果:

  • 分析时不可达的代码(比如没被调用的方法、用反射才触达的类)会被直接丢弃,不在可执行文件里。
  • 反射、动态代理、资源加载、序列化,编译器看不见,必须显式告诉它。
  • 类路径在构建时固定,运行时不能加依赖。
  • 没有懒加载,启动时所有类都装进内存(好在没有 JVM 的元数据开销)。

一句话:分析期看不到的东西,运行期就没有。

Spring 的动态性 vs 原生镜像

Spring Boot 天生「动态」:自动配置根据 classpath 和属性决定装什么 Bean,@ConditionalOnXxx 一堆条件注解,Bean 实例化走反射。这些动态行为,静态分析全看不见。

Spring 的解法是 Spring AOT 处理:构建时先把应用启动到「Bean 定义就绪」的阶段,把动态决策提前做完,生成三类产物:

  • Java 源码:把 Bean 定义转成直接的代码(Maven 下在 target/spring-aot/main/sources)。
  • 字节码:动态代理类提前生成。
  • JSON 提示文件:放在 META-INF/native-image/ 下,告诉 GraalVM 反射、资源、序列化、JNI 的清单:
    • reflect-config.json(反射)
    • resource-config.json(资源)
    • serialization-config.json(序列化)
    • proxy-config.json(动态代理)
    • jni-config.json(JNI)

GraalVM 编译时读取这些提示文件,把动态部分也编进可执行文件。这就是「闭世界假设」:应用的结构在构建时定死,运行时不再变化。

构建原生镜像

准备环境

两种方式二选一:

  1. 安装 GraalVM JDK(自带 Native Image 工具),用 GRAALVM_HOME 指向它。
  2. 用普通 JDK 21+,装 Native Build Tools 插件(自动下载 GraalVM 组件)。21 LTS 可用,官方示例为 25。
Note

Spring Boot 4.x 中,原生镜像是一等公民。用 Spring Initializr 创建项目时勾选 GraalVM Native Supportpom.xml 里会自动带上 native 的 Maven Profile 和 org.graalvm.buildtools:native-maven-plugin,不用手写。

Maven 构建

项目根目录执行:

mvn -Pnative native:compile

构建产物在 target/ 下,文件名是 demo(没有 .jar 后缀):

./target/demo

直接运行,没有 java 命令:

./target/demo --server.port=8080

第一次构建很慢:要跑 AOT 处理、静态分析、编译,几分钟很正常。构建机内存建议 8GB 以上。

构建原生镜像容器

应用要进 Docker,直接构建原生可执行文件放进最小镜像:

mvn -Pnative spring-boot:build-image

这条命令用 Buildpacks 生成一个包含原生可执行文件的容器镜像,基础镜像极小(不带 JVM),镜像体积从几百 MB 降到几十 MB。也可以从 Dockerfile.native 手动构建,思路一致:构建阶段出可执行文件,运行阶段用 paketobuildpacks/run 之类的轻量基础镜像。

运行时行为差异

原生镜像跑起来,行为和 JVM 版有几处不同:

  • Profile 受限@Profile 和 profile 专属配置的支持有限,构建时就要确定。
  • 条件属性受限@ConditionalOnProperty 这类「运行时才定」的 Bean 条件不支持。
  • 反射需声明:自己写的反射代码要加 @RegisterReflectionForBinding 等注解或补 hint 文件。
  • 文件路径、类路径操作:部分 JDK API 行为不同。
  • 懒加载失效spring.main.lazy-initialization=true 在原生镜像下无意义。

所以原生镜像不是「无脑切换」。官方原则:先让应用在 JVM 下全绿,再打原生镜像,遇到问题逐项补 hint。

什么时候用原生镜像

适合

  • 函数计算 / Serverless,按调用次数计费,启动越快越省。
  • 容器频繁扩缩容,秒级就绪是关键指标。
  • 内存敏感的小服务,能省一半内存。
  • CLI 工具类应用,秒开体验好。

不适合

  • 大型单体,构建时间太长,反射场景复杂。
  • 重度动态特性:大量反射、字节码增强、运行时生成类。
  • 需要运行时热部署、动态加载插件的场景。
Tip

判断项目适不适合,先跑一次 mvn -Pnative native:compile。构建失败信息会直接告诉你有哪个反射没声明。小服务通常改几处就能过。

折中方案:AOT 缓存与 CDS

不想换 GraalVM,又想加快启动?JVM 也有 AOT 类特性:

  • CDS(类数据共享):Java 24 及以下,训练一次生成 .jsa 归档,启动时直接加载。
  • AOT 缓存:Java 25+ 的新特性,-XX:AOTCacheOutput 生成、-XX:AOTCache 加载。
# 训练(解压形态的应用)
java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar demo.jar

# 带缓存启动
java -XX:AOTCache=app.aot -jar demo.jar

效果是启动时间明显缩短,代价远小于原生镜像,是「性价比」路线。第 45 章会结合部署一起讲。

小结

  • 原生镜像 = 构建时 AOT 编译成机器码,启动快、内存小、无需 JVM。
  • Spring AOT 处理生成源码、代理字节码和 JSON hint 文件,解决动态问题。
  • 构建命令:mvn -Pnative native:compile;容器镜像:mvn -Pnative spring-boot:build-image
  • 反射、Profile、条件属性是主要限制,适合小服务和无服务器场景。