# 尝试了一下 Java 的 GraalVM Native / AOT:确实更省内存、更快启动,但现阶段不适合我 #
把一个 Spring Boot 多模块后端(AxT Dashboard Backend)尝试用 GraalVM Native Image / AOT 编译成原生可执行文件。
结论先放在前面:
- 运行层面很香:内存占用能做到 20-40MB,启动也明显更快。
- 但构建层面很劝退:单模块 Native 构建至少 10 分钟,并且对机器要求很高(我这次测试环境 8C / 8G,4G 会 OOM)。
所以目前我的策略是:方案跑通,形成脚本与配置,先归档。等未来真要全量转 Native,再把这套东西拿出来继续推进。
在此之前我也查询过很多资料,比如这篇Reddit讨论: GraalVM 是首选吗
1. 背景:我为什么要折腾 Native Image #
我最开始的动机其实很朴素:
- GraalVM Native Image 与传统 JVM 内存管理:云原生时代的技术选型指南
- Spring Boot 服务跑起来后,JVM 进程常驻内存不低(尤其一堆模块拆开跑)。
- 我的 6个微服务模块,实测 docker 上机后单个容器 430MB+,刚启动3G内存没了
- 为了节省成本,目前生产机器 4C4G,可能还需要蓝绿部署,双倍内存成本暴增
- 看看 Native Image 能不能让后端服务在测试/轻量环境里“更轻”。
- 高并发场景下内存的使用量甚至可以比肩Go语言
GraalVM Native Image 的卖点也很明确:
- 更快启动(省掉 JIT 预热)
- 更低内存(很多场景能显著下降)
但它的代价同样出名:
- 构建慢
- 兼容性坑多(反射、动态代理、资源加载、Spring Cloud 特性……)
我这次做的不是“看文档跑 demo”,而是直接把它落到真实项目上:
- 后端是 Maven 多模块
- Spring Boot 3.5.x + Java 17
- 用了 Spring Cloud Config(配置中心)
2. 在选 Native 之前,我做过什么 #
在决定上 Native 之前,我其实是按“正常工程思路”先评估/尝试了一圈:
- 先接受 JVM 的现实:服务多模块拆开跑,单个服务不大,但整体常驻内存并不低。
- 先把可观测性拉起来:关注的核心指标很明确:
- 启动时间
- 空闲内存占用
- 压测/正常流量下的稳定性
- 再看“跑得更省”的路线:我需要的是能在测试环境更轻量地跑起来,而不是极致吞吐。
- 尝试通过JVM参数缩小占用: 使用了 -Xms,-Xss,-XX:MaxMetaspaceSize 等一系列限制,但内存占用仍然没有可观的减少,反而会导致启动慢的特性
最终我选择 Native 的原因也很简单:
- 我更在意“启动更快、更低内存”这两个点。
- 我可以接受“先把路跑通、先归档”的策略,而不是一步到位全量迁移。
3. 环境与目标 #
3.1 构建环境 #
- OS: Ubuntu 24.04
- CPU: 8 Core
- RAM: 8 GB(实践证明 4 GB 会 OOM)
- GraalVM:
17.0.9-graalce - Maven: 3.9+
3.2 目标 #
我希望做到:
- 在远程编译机上完成单个模块的 Native Image 构建
- 编译成功后直接输出可执行文件路径
4. 首要问题: AOT 阶段会“启动一部分应用”,Config Server 连接直接炸 #
Native Image 构建阶段(更准确地说是 AOT processing)会触发 Spring Boot 的一系列初始化。
而我的服务用了 Spring Cloud Config:
- 默认会去连
http://localhost:28610
构建机上当然没有 Config Server,于是报错:
ConfigClientFailFastException- 连接被拒绝
5.1 我当时的疑问 #
“是不是必须完整启动一遍服务才能构建?”
答案是:
- 不需要完整启动
- 但 AOT 阶段确实会做很多启动期事情,所以像 Config Client 这种“引导期外部依赖”,会在构建时就触发。
4.2 解决方案:让 Config Server 变成“可选” #
这里我踩到的关键点是:AOT 期间会触发引导期配置加载。如果引导期就强依赖外部服务(例如 Config Server),构建就会直接失败。
我的修复做法是两步:
- 把引导期导入改为可选:
spring:
config:
import: "optional:configserver:"
- 关闭 fail-fast(让“连不上配置中心”变成 warn,而不是直接终止):
spring:
cloud:
config:
fail-fast: false
这样构建时即使 Config Server 不在线,也只会打印 WARN,不会把 AOT 直接打断。
- 只改
fail-fast不一定够optional:configserver:才是让引导期导入真正“可选”的开关
5. 下一波坑:@RefreshScope 这类 Spring Cloud 特性与 Native 不友好
#
Config Server 的连接问题解决后,AOT 继续往下跑,又遇到:
AotBeanProcessingException- 出错 bean:
OAuthProperties
我定位到这个配置类上有:
@RefreshScope
@RefreshScope 本质是运行时动态刷新 + 代理增强(用于运行时动态刷新配置服务),在 Native Image 下经常会踩到 AOT/代理/反射相关坑。
5.1 解决方案 #
在这次“先把构建跑通”的阶段,我选择了最直接的处理:
- 移除
@RefreshScope(至少先让 AOT 能过,再谈更优雅的方案)
6. 反射配置:reflect-config.json 还是要准备
#
即使 Spring Boot 3 对 AOT/Native 已经友好很多,但项目里只要有:
- 反射
- Jackson 序列化
- 一些框架自动装配
就很容易遇到运行期找不到构造器/字段之类的问题。
这次为了快速跑通,我把本次验证过的一份反射配置也一起整理归档(方便后续复用/扩展)。
7. 最终结果:确实能跑,但构建成本太高 #
8.1 结果 #
- ✅ Native Image 编译成功
- ✅ 可执行文件能跑起来
- ✅ 内存占用明显下降:20-40MB
- ✅ 启动速度更快(非常明显,几乎全程加载完10s内,正常JVM则1m左右)
8.2 现实问题(也是我选择暂缓的原因) #
- ❌ 构建时间太长:单模块至少 10 分钟
- ❌ 构建资源消耗太大:
- 8C/8G 才比较顺
- 4G 直接 OOM(这点对我的测试/CI环境不友好)
- 构建出的二进制文件比原jar文件大了3倍
因此对我当前阶段来说:
- Native Image 不是不能用
- 但前期投入产出比不高,尤其我这里还是多模块(模块越多,构建成本线性甚至更糟)。
8. 归档:把“跑通所需的一切”统一收口 #
我最终决定把这套方案“归档而不是删除”,原因很简单:
- 以后如果真的要全量转 Native,这些踩坑经验能直接复用
归档内容主要包括几类:
- 构建脚本:远程构建、输出产物路径
- POM 片段:如何启用 native profile / 插件
- 反射配置示例:本次验证通过的基础版本
- 关键配置改动清单:AOT/Native 相关的必要调整点
同时,为了不影响日常生产/开发体验,我也做了回滚/恢复:
- 恢复
@RefreshScope - 恢复
application.yml的 config server 导入方式 - 移除根 POM 的 native profile
- 移除模块 POM 的 native 插件
- 清理模块内的
META-INF/native-image(避免长期污染)
9. 我对“是否要上 Native Image”的个人建议 #
如果是下面情况,Native Image 会很香:
- 需要极致启动速度(例如 serverless / 弹性扩缩容特别频繁)
- 机器内存特别紧张
- 项目对 Spring Cloud 依赖不重(或者你愿意做适配/替换)
如果是下面情况,建议先观望:
- 多模块 + 依赖复杂 + Spring Cloud 特性多
- CI 资源一般(尤其 RAM < 8G)
- 更在意“构建效率/迭代速度”而不是“运行时极致性能”
我这次的选择就是:
- 现在先不全量上
- 把路走通并归档
等未来时机成熟(比如:模块更少、配置中心方案调整、CI 机器更强、业务更需要低内存/快启动),再把它捡起来继续。