1. 为什么突然都在聊GraalVM
先说个现象:最近后台留言和社群里被问得最多的几个问题,一个是“怎么把Java项目打成exe”,另一个是“Spring Boot能不能换掉默认的打包方式”。这俩问题指向同一个答案——GraalVM。
GraalVM不是新鲜东西,Oracle从2018年开始就在推,但真正让它火起来的原因很现实:云原生时代大家嫌Java启动慢、内存吃得凶,而GraalVM的native-image功能能把Java应用直接编译成原生可执行文件,启动时间从秒级降到毫秒级,内存占用也大幅下降。再加上Spring Boot 3.0官方开始支持GraalVM原生镜像,热度一下就上来了。
这篇博文是我自己在Windows上从零安装、配置、跑通GraalVM全流程的记录,包括环境准备、组件安装、native-image打包exe,以及一堆实操中踩过的坑。适合这几类人看:想给Java项目瘦身加速的老手、初学Java但想搞明白“Java怎么变成exe”的新人、以及在做工具类小项目想分发给没有Java环境的用户的朋友。
先说明一点:GraalVM在Windows上的体验跟Linux不完全一样,坑更多,但也完全走得通。我会尽量把每一步背后的原因讲清楚,而不只是丢给你一串命令。
2. 先说清楚GraalVM到底是什么
2.1 它不只是一个JVM
很多人以为GraalVM就是个“更快一点的JDK”,这个理解不完整。
GraalVM的本质是一个支持多语言的高性能运行时,核心组件包括:
- Graal编译器:用Java写的JIT编译器,可以动态地把热点代码编译成高性能机器码,在JIT模式下通常比传统C2编译器快10%-20%。
- Truffle框架:基于Graal编译器的一套语言实现框架,用来跑JS、Python、Ruby、R、LLVM等语言。
- Native Image:AOT(Ahead-Of-Time)编译器,把Java字节码直接编译成独立可执行文件,不需要JVM就能运行。
- Polyglot API:是GraalVM最有意思的特性之一,你可以在Java里直接调用JS代码,或者在JS里调用Java类。
也就是说,GraalVM有三种使用姿势:
- 当普通JDK用:替换掉OpenJDK,跑现有Java程序。
- 当多语言运行时用:比如在Java项目里跑一段JavaScript脚本,做规则引擎之类的。
- 当编译器用:用native-image把你的应用编译成原生可执行文件。
2.2 native-image到底做了什么
传统Java程序的运行方式是这样的:java命令启动JVM,JVM加载class文件,逐行解释执行,遇到热点代码再用JIT编译成机器码。好处是跨平台,坏处是启动慢、预热需要时间、内存开销大。
native-image的方式完全不同。它把编译过程提前到部署之前:分析你的类和所有依赖,执行静态分析,确定哪些代码可以被访问,然后生成一个包含了应用代码、依赖库、运行时组件和垃圾回收器的独立可执行文件。
这个文件有以下几个特点:
- 不需要JVM,直接调用操作系统的执行机制。
- 启动速度极快,通常50ms以内。
- 内存占用大幅降低,很多项目实测能降到原来的1/3到1/5。
- 文件体积较大,一个简单的Hello World原生镜像可能就有10MB+。
代价也很明显:它不支持完整的Java反射、动态代理、JNI等动态特性,需要额外配置。如果你完全不懂这些限制,直接把Spring Cloud项目扔给native-image,大概率会失败。
2.3 关于GraalVM JDK版本的说明
GraalVM有两个主要版本线:社区版(CE,GraalVM Community Edition)和企业版(EE,GraalVM Enterprise Edition)。社区版免费,基于OpenJDK,企业版需要许可,性能更强。
版本号命名上要注意:GraalVM for JDK 17、GraalVM for JDK 21这样的说法,指的是GraalVM基于哪个JDK版本构建的。我写这篇教程时推荐下载GraalVM for JDK 21,因为它对应的是LTS版本,兼容性最好。GraalVM for JDK 23这种新版本可以尝鲜,但生产环境不推荐。
3. Windows环境准备与安装全流程
3.1 确认你的Windows系统版本
GraalVM官方要求Windows 10及以上,64位系统是必须的,不支持32位。安装前建议先升级系统补丁,避免某些奇怪的兼容性问题。
我的测试环境是Windows 11 22H2、64位系统、16GB内存、Intel i5,磁盘剩余空间至少预留5GB。这些条件里,内存和硬盘空间是硬指标——native-image打包时非常吃内存,我后面会详细讲。
在开始之前,建议先把以下信息搞清楚:
- 系统是Windows 10还是Windows 11(查看方法:设置→系统→关于→Windows规格)
- 系统是64位还是32位(右键“此电脑”选“属性”,在“系统类型”里看)
- 当前是否已安装JDK、版本是多少(命令行执行
java -version)
3.2 JDK的取舍:卸载旧版还是共存
这是安装前最常见的纠结。GraalVM内置了一个完整JDK,能独立运行,不需要先装其他JDK。我的建议是:如果你电脑上已有其他JDK,先不要卸载,等GraalVM装好、确认能正常用了,再决定是否清理。
多个JDK共存完全没问题,关键是把JAVA_HOME环境变量指对。比如你之前装了JDK 8,现在要用GraalVM 21,只要改JAVA_HOME和PATH里的路径就行。
有人问我“装了GraalVM之后原来的项目会不会跑不起来”——不会,只要你在项目里重新指定JDK路径就行。IDE(IDEA、Eclipse等)都可以在项目级别配置JDK,跟全局的JAVA_HOME解耦。
3.3 下载GraalVM的正确方式
GraalVM的下载渠道有两个:Oracle官网和GitHub Release页面。
去GitHub下载地址(github.com/graalvm/graalvm-ce-builds/releases)会更直观,每个版本会列出所有平台对应的文件。注意Windows平台有三个文件:
- graalvm-community-jdk-21.0.2_windows-x64_bin.zip
- graalvm-community-jdk-21.0.2_windows-x64_bin.msi
- graalvm-community-jdk-21.0.2_windows-x64_bin-src.zip
推荐下载MSI安装包,因为它会自动配置环境变量并创建卸载入口,对新手最友好。ZIP包适合需要自定义安装位置的高级用户。SRC是源码包,日常使用不用下。
文件大小在300MB左右。下载时留意硬盘空间,解压后还需要额外空间。
3.4 通过MSI安装包安装
双击MSI文件,弹出的安装向导跟普通Windows软件没太大区别。但有几个关键选项要注意:
- 安装路径不要带中文和空格。默认路径是
C:\Program Files\Java\graalvm-community-jdk-21.0.2,这个路径里有空格,有时候个别工具会出问题。我建议改到C:\Java\graalvm-jdk-21这样的路径。 - 安装向导会询问是否添加JAVA_HOME环境变量——这里务必勾选。
- 安装向导还会问是否将GraalVM添加到PATH,也勾选上。
- 安装过程需要管理员权限,建议右键“以管理员身份运行”安装程序。
安装完成后,打开新的命令行窗口(重要:旧窗口不会加载新环境变量),执行以下命令验证:
java -version javac -version如果看到类似下面的输出说明基本装好了:
openjdk version "21.0.2" 2024-01-16 OpenJDK Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.0-b35) OpenJDK 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.0-b35, mixed mode, sharing)注意第一行是openjdk version开头,里面包含了“GraalVM CE”字样,这样就对了。如果显示的还是你旧版本JDK的信息,说明PATH顺序有问题,后面会讲怎么修。
3.5 手动安装ZIP包的步骤
如果你选择ZIP包,解压到目标目录后,需要手动配置环境变量。
打开系统属性(Win+R输入sysdm.cpl,或者右键“此电脑”→属性→高级系统设置)→环境变量。在“系统变量”区域做以下操作:
- 新建变量
JAVA_HOME,变量值填GraalVM解压后的根目录,比如C:\Java\graalvm-community-jdk-21.0.2 - 在
Path变量的值末尾追加%JAVA_HOME%\bin。注意如果Path里已经有其他JDK的bin路径,要确保GraalVM这个路径排在前面。 - 新建变量
GRAALVM_HOME,变量值跟JAVA_HOME一样。这个变量后面给IDEA用。
修改完成后,务必重新打开命令行窗口再验证。
3.6 安装native-image组件
GraalVM的核心能力是native-image,但这个组件默认不随主安装包一起安装,需要额外下载。
打开命令行,执行:
gu install native-image这里用到的gu是GraalVM Updater工具,类似Python的pip。它会自动从官网下载并安装native-image组件。
下载速度可能比较慢,如果卡住或失败,可以多试几次,或者配置镜像源。配置镜像源的方式是编辑graalvm_home\lib\installer目录下的配置,这个有点复杂,我建议先直接重试。
安装完成后,执行:
native-image --version看到版本信息就说明装好了。
3.7 安装Windows版C++编译工具链
这是Windows平台最关键的额外步骤,也是坑最多的地方。
native-image在Windows上编译时,底层会调用MSVC(Microsoft Visual C++)编译器来链接原生代码。没有这个工具链,你在后续的打包过程中会看到一堆奇怪的链接错误。
请记住:必须安装Visual Studio Build Tools。有两种安装方式:
方式一:完整安装Visual Studio
从Visual Studio官网下载安装程序,选择“使用C++的桌面开发”工作负载。这个方式装的东西多、占用空间大,但对Windows开发者来说是常规操作。
方式二:只装Build Tools(推荐)
从微软官网搜索“Visual Studio Build Tools 2022”,下载独立安装器。安装时同样勾选“使用C++的桌面开发”,右侧确保包含以下组件:
- MSVC v143 - VS 2022 C++ x64/x86 生成工具
- Windows 11/10 SDK
- Windows SDK中的CMake工具
- 适用于最新v143生成工具的C++ ATL
整个安装过程需要下载大量文件,大约占用7-10GB磁盘空间,请留出余量。
3.8 验证所有组件的组合状态
到这里,完整的安装流程就结束了。用一个综合命令检查全部环境信息:
java -version javac -version native-image --version gu --version echo %JAVA_HOME% echo %GRAALVM_HOME%如果全部正常输出,恭喜你,最难的安装环节已过,接下来进入使用阶段。
4. 应用实战:Spring Boot打包exe的关键路径
4.1 先跑通一个Java程序
在真正折腾Spring Boot之前,我强烈建议先写一个没有外部依赖的Java类,用native-image编译一次,把流程走通。这一步能帮你快速区分“GraalVM基础操作”和“项目级配置”两类问题。
我们写一个父类示例:
public class HelloGraal { public static void main(String[] args) { System.out.println("Hello, World!"); } }编译并运行:
javac HelloGraal.java java HelloGraal然后用native-image编译成原生镜像:
native-image HelloGraal这条命令会在当前目录生成HelloGraal.exe。如果你是直接在开发者的命令提示符(Developer Command Prompt for VS 2022)里执行的,这一步会顺利通过。如果是在普通cmd里执行,大概率会报错。
这个exe的启动速度,你在命令行里敲HelloGraal回车,几乎是瞬间打印出结果。跟之前java HelloGraal的延迟相比,体感完全不同。
4.2 在IDEA中配置GraalVM
IDEA是目前用得最多的Java IDE,配置GraalVM的步骤不复杂。
打开File → Project Structure → Project → SDK,点击“Add SDK”按钮,选择“Download JDK”或“Add JDK from disk”。如果之前已配置过GraalVM,也可以直接下拉选择。
关键点:还要在Settings → Build, Execution, Deployment → Build Tools → Maven(或Gradle)里,把JRE/Java Home切换成GraalVM 21的路径。因为Maven在打包时用的是“Java Home”配置,而不是SDK配置。
IDEA识别到GraalVM后,插件市场会自动推荐GraalVM相关插件,比如“GraalVM”官方插件和“Native Image Viewer”。建议都装上,前者提供本地运行支持,后者能可视化native-image的构建过程。
4.3 Spring Boot 3项目开启native-image
Spring Boot项目用GraalVM打包,核心是使用Spring Boot 3.0及以上版本。我实测下来,Spring Boot 3.2之后的兼容性最稳,3.0和3.1在某些版本上有反射注册的小bug。
以一个最简单的Spring Boot Web项目为例。先建一个控制器:
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, GraalVM!"; } }在pom.xml中做几件事:
配置GraalVM native插件(来自Spring Boot官方):
<profiles> <profile> <id>native</id> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>0.10.2</version> <extensions>true</extensions> <configuration> <imageName>demo-native</imageName> </configuration> <executions> <execution> <id>build-native</id> <goals> <goal>compile-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles>在项目的根目录执行:
mvn -Pnative clean package注意这个操作非常重,依赖多、构建时间长。我第一次打一个简单的Spring Boot项目,大概花了3到5分钟。构建过程中,native-image会分析所有可达代码路径,内存占用可能飙到4GB以上。
构建完成后,在target目录下会生成一个可执行文件。如果项目名是demo,它会生成demo-native.exe。直接运行:
demo-native.exe然后在浏览器访问http://localhost:8080/hello,成功返回字符串。注意对比启动日志——native镜像的启动日志是瞬间打出来的,根本没有Spring Boot那串长长的banner渐进过程,这是最直观的体验差异。
4.4 可执行jar和native-image的适用场景对比
这里我必须说清楚:不是所有项目都适合打native-image。我做了两个对比维度:
启动时间方面,传统jar包启动一个Spring Boot应用通常需要2-8秒,native-image基本都是毫秒级,适合追求秒开体验的场景,比如命令行工具、无服务器函数、边缘计算节点。
运行内存方面,传统JVM常驻内存至少几百MB,native-image能把整个应用压到几十MB甚至更低。我打过一个小型REST服务,native镜像运行时内存占用约60MB,同一个应用用JVM跑占用约250MB。
但代价是:native镜像构建时间长、产物体积大(一个简单的Spring Boot应用native镜像通常80-120MB),而且反射、序列化、动态代理都需要额外声明配置。如果项目大量依赖反射机制,比如用了MyBatis Plus、Hibernate的延迟加载功能,改造成本会很高。
因此我的判断标准是明确的:如果是给内部用的命令行工具,或者部署在云函数上,native-image是绝佳选择;如果是典型的Web后端服务、代码里重度使用了反射或动态代理,还是老老实实打jar包,别折腾。
5. 常用功能演示与多语言能力
5.1 用GraalVM跑JavaScript
装好GraalVM之后,系统里自带了一个js命令,可以直接运行JS脚本。
新建一个hello.js:
console.log("Hello from JavaScript!");然后在命令行执行:
js hello.js你会看到输出。这看起来平平无奇,但背后是GraalVM的Truffle框架在发挥作用。
更强大的是在Java和JS之间互调。比如你想在Java里调用某个JS函数:
function add(a, b) { return a + b; }Java代码:
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; public class JsInJava { public static void main(String[] args) { try (Context context = Context.create()) { Value function = context.eval("js", "function add(a, b) { return a + b; } add;"); Value result = function.execute(10, 20); System.out.println("Result: " + result.asInt()); } } }编译运行后输出Result: 30。
这种跨语言调用能力在业务系统中用处很大。比如你的规则引擎想让业务人员用脚本写规则,但应用主体是Java,传统的做法是用Groovy或MVEL,而GraalVM直接支持多种语言,且性能更好。
5.2 用Polyglot实现语言混编
Polyglot是GraalVM的灵魂。它不止在一个进程里同时跑多种语言,还能让不同语言之间共享对象和数据。
一个典型的用法:把系统里经常变化的策略计算部分用JS编写,Java负责核心业务逻辑。这样,策略变更时不用重新编译整个Java应用,只需要替换JS脚本文件。
我在实际项目中曾经用GraalVM polyglot做过一次规则引擎改造,把一个原本需要用Java写死的折扣计算逻辑,替换成外部JS脚本。上线之后的效果是:业务调价不再需要重启应用,而且因为GraalVM对JS的JIT优化,计算性能几乎没有下降。
5.3 其他语言支持一览
GraalVM社区版还支持Python(实验性)、R(实验性)、Ruby(实验性)、LLVM。不过说句实在话,这些实验性支持更适合技术研究,生产环境慎用。原因很简单:实验性组件的API可能随版本变化,出了问题社区也不一定来得及响应。
最稳的多语言搭配是Java + JavaScript,Java + Python次之。
6. Windows下常见报错与排查方法
6.1 mvn包时报错:Unable to detect supported target
这个问题常见于native-image构建时找不到对应的编译工具。它会提示检查MSVC编译器是否可用。
解决办法是:打开“Developer Command Prompt for VS 2022”(Visual Studio安装时会添加这个快捷方式),然后再执行构建命令。注意,不是所有的“以管理员身份”命令行都包含MSVC的环境变量,必须是从Visual Studio派生出来的那个命令行才带上。
如果找不到这个命令行入口,还有一个兜底方法:直接在native-image构建前执行VS的vcvars64.bat初始化脚本,这个脚本位于Visual Studio安装目录的VC\Auxiliary\Build下。
6.2 native-image执行报错:Directory not found
这个经常出现在环境变量路径配置不对的场景,比如JAVA_HOME指向了一个不存在的目录。
排查思路:
echo %JAVA_HOME%确认路径是否存在where java查看当前解析到的java路径是哪个- 确认系统变量和用户变量里有没有重复的JAVA_HOME定义,有重复时系统变量优先
6.3 打包时内存不足
native-image构建是非常消耗资源的操作。我自己的经验是:至少在16GB内存的机器上跑才舒服,如果只有8GB内存,建议先关掉所有浏览器和其他大型应用,再执行构建。
可以通过JVM参数控制native-image使用的内存:
native-image -J-Xmx8g HelloGraal-J-Xmx8g表示给native-image的构建进程分配8GB堆内存。如果电脑配置一般,建议设为6g或4g。
6.4 反射、序列化、代理类不生效的坑
这是native-image最大的坑。传统Java项目里,Spring用反射创建Bean,MyBatis用动态代理生成Mapper实现,Jackson用反射做序列化/反序列化。这些在“无JVM”的原生镜像里默认全部失效。
解决方式是通过reflect-config.json、proxy-config.json等配置文件提前注册类信息。Spring Boot 3提供了自动配置机制,可以生成大部分reflect-config.json。
如果是非Spring项目,可以直接用native-image参数:
native-image -H:ReflectionConfigurationFiles=reflect-config.json HelloGraalreflect-config.json格式:
[ { "name": "com.example.MyClass", "methods": [ { "name": "sayHello", "parameterTypes": [] } ] } ]一句话总结:遇到类加载失败、ClassNotFoundException、NoSuchMethodError等问题时,先怀疑是不是反射配置缺失。
6.5 native-image生成exe无法在别的电脑运行
这个问题不少人踩过。在Windows上生成的exe依赖VC++运行库,目标机器如果没有安装VC++ Redis软件包(Microsoft Visual C++ Redistributable),运行时会报错“VCRUNTIME140.dll找不到”。
解决方案有两个:一是把VC++运行库安装程序一起发给用户;二是在编译时做静态链接,把运行库打进exe里(配置较复杂,不推荐新手尝试)。
6.6 不同路径包含中文或空格造成的坑
这个真的值得单独说。Windows上很多用户习惯把项目放在C:\Users\小明\workspace\我的项目\这种路径下,native-image对这种路径的处理有时候会出现诡异的编码问题。
如果构建过程中出现奇怪的编码异常或路径解析错误,先尝试把项目移到纯英文路径下再构建。虽然理论上现代版本应该支持Unicode路径,但实测偶尔还是会翻车。
6.7 环境变量更新后命令行认不出来
改完环境变量,执行java -version还是旧版本。原因通常是两个:命令行窗口没重启,或者系统Path里旧JDK路径排在前面。
解决办法:关闭所有命令行窗口后重开。确认顺序:在环境变量面板里上下移动条目,确保%JAVA_HOME%\bin在旧JDK路径之前。
6.8 下载安装包超时或失败
GraalVM的安装文件比较大,网络不稳定时容易失败。建议使用支持断点续传的下载工具,或者从GitHub Release页面选择镜像源下载。
gu install失败的情况,可以先检查gu env查看当前配置的用户目录,有时候用户目录含中文也会导致解析失败。
7. 实操心得与避坑技巧总结
7.1 开发阶段保持双模式并行
我现在的建议是:在开发阶段继续用JVM模式跑项目,能大幅缩短迭代周期。
对比一下:打一个Spring Boot原生镜像通常需要几分钟,如果你每改一行代码就要重新打镜像,完全是灾难。正确的做法是:
- 开发调试阶段:使用传统JVM模式,即时生效。
- 测试阶段:用native-image打包后冒烟测试,验证关键功能的兼容性。
- 发布阶段:视场景决定是发jar包还是exe。
7.2 优先使用GraalVM 21及以上版本
我在低版本上踩过不少莫名其妙的坑,比如20.x版本对Win11的兼容性问题、对某些JDK版本识别错误等。升级到GraalVM 21之后,稳定性有了明显提升。如果有条件,建议直接用当前最新的稳定版。
7.3 关注反射和配置的即时性
凡是涉及反射、资源文件、动态代理的三方库,都要在引入时留意它的GraalVM适配情况。好在主流框架的控制台或官方文档里基本都有native-image支持说明。遇到不兼容的库,可以先看看有没有替代库,或者通过配置中心化解决。
7.4 小心Gradle和Maven混用导致的构建链差异
Gradle对GraalVM的支持比Maven晚几年,但现在已经很成熟。要注意的是,同一个项目不要在Gradle和Maven之间来回切换构建方式,一旦native-image的配置生成方式变了,原先适配的一些元数据配置容易失效。
我的经验是:如果一开始用Maven,就一直用Maven;如果团队统一用Gradle,就保持Gradle。切换构建工具的成本远比你想象的高。
7.5 用类路径避免依赖遗漏
编写传统的Java程序时,classpath通常需要手动指定依赖。比如:
java -cp .;lib/* com.example.Main在native-image构建时,要确保所有依赖都正确加入classpath:
native-image -cp ".;lib/*" -jar myapp.jar myapp如果依赖没有完整加进classpath,native-image的静态分析阶段可能无法发现类,构建过程会报错,而且错误信息往往很模糊。
7.6 构建日志与失败分析技巧
构建失败时,不要只看最后几行错误。native-image构建过程非常长,真正的错误通常出现在“Analyzing”阶段和“Building”阶段之间。建议把构建日志重定向到文件,方便排查:
mvn -Pnative clean package > build.log 2>&1然后搜索关键字Fatal、Error、Exception。如果错误堆栈里出现了具体类名,比如java.lang.ClassNotFoundException: com.example.Xxx,那基本就是反射配置问题。
7.7 大项目分模块做一个先例验证
我有一个很实用的习惯:第一次给某个大型项目做native-image适配时,先建一个极小的模块,只引入该项目最核心的依赖和一段最小化入口代码,验证这一批依赖能否通过native-image编译。通过后再逐步扩大范围。这个方法能帮你把复杂问题隔离出来,不至于在几百个类里找“哪个类触发了这个反射错误”。
8. GraalVM后续还能怎么玩
GraalVM的整个生态还在快速迭代,几个值得关注的后续方向:
一是NaC(Native Compilation)在云原生领域的应用。很多云服务商已经支持基于GraalVM的Spring Boot Serverless部署。
二是GraalVM与GraalVM Native Build Tools的持续演进,对反射和代理的支持越来越完善,尤其是GraalVM 22.2之后新增的Tracing Agent,能自动收集运行过程中的反射调用并生成配置文件,大幅降低了适配成本。
三是GraalPy(Python实现)、Ruby等语言的逐步成熟。虽然现在标记为实验性,但Oracle的投入方向很明确,未来在数据科学和AI领域可能会出现更多交叉用法。
就我个人而言,最直观的体会是:以前给客户写工具,总得要求对方装JDK、配置JAVA_HOME、弄一堆环境变量;现在直接用native-image打一个exe丢过去,双击就能跑,省掉的不只是沟通成本,还有那种“怎么你机器上跑不起来”的挫败感。
最后再分享一个小技巧:千万别把JAVA_HOME直接指向GraalVM的安装目录之后,就把系统里原来的JDK删掉。等你的所有项目都确定能在GraalVM下正常工作、所有依赖库都验证过兼容性之后,再清理也不迟。GraalVM虽然很强,但它在Windows上的生态位还不至于让传统JDK提前退休,两条腿走路永远是更稳的姿势。