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 ImageAOTAhead-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命令启动JVMJVM加载class文件逐行解释执行遇到热点代码再用JIT编译成机器码。好处是跨平台坏处是启动慢、预热需要时间、内存开销大。native-image的方式完全不同。它把编译过程提前到部署之前分析你的类和所有依赖执行静态分析确定哪些代码可以被访问然后生成一个包含了应用代码、依赖库、运行时组件和垃圾回收器的独立可执行文件。这个文件有以下几个特点不需要JVM直接调用操作系统的执行机制。启动速度极快通常50ms以内。内存占用大幅降低很多项目实测能降到原来的1/3到1/5。文件体积较大一个简单的Hello World原生镜像可能就有10MB。代价也很明显它不支持完整的Java反射、动态代理、JNI等动态特性需要额外配置。如果你完全不懂这些限制直接把Spring Cloud项目扔给native-image大概率会失败。2.3 关于GraalVM JDK版本的说明GraalVM有两个主要版本线社区版CEGraalVM Community Edition和企业版EEGraalVM 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 -version3.2 JDK的取舍卸载旧版还是共存这是安装前最常见的纠结。GraalVM内置了一个完整JDK能独立运行不需要先装其他JDK。我的建议是如果你电脑上已有其他JDK先不要卸载等GraalVM装好、确认能正常用了再决定是否清理。多个JDK共存完全没问题关键是把JAVA_HOME环境变量指对。比如你之前装了JDK 8现在要用GraalVM 21只要改JAVA_HOME和PATH里的路径就行。有人问我“装了GraalVM之后原来的项目会不会跑不起来”——不会只要你在项目里重新指定JDK路径就行。IDEIDEA、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.zipgraalvm-community-jdk-21.0.2_windows-x64_bin.msigraalvm-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.213.1 (build 21.0.213-jvmci-23.0-b35) OpenJDK 64-Bit Server VM GraalVM CE 21.0.213.1 (build 21.0.213-jvmci-23.0-b35, mixed mode, sharing)注意第一行是openjdk version开头里面包含了“GraalVM CE”字样这样就对了。如果显示的还是你旧版本JDK的信息说明PATH顺序有问题后面会讲怎么修。3.5 手动安装ZIP包的步骤如果你选择ZIP包解压到目标目录后需要手动配置环境变量。打开系统属性WinR输入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上编译时底层会调用MSVCMicrosoft 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 SDKWindows 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中配置GraalVMIDEA是目前用得最多的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-imageSpring 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 idnative/id build plugins plugin groupIdorg.graalvm.buildtools/groupId artifactIdnative-maven-plugin/artifactId version0.10.2/version extensionstrue/extensions configuration imageNamedemo-native/imageName /configuration executions execution idbuild-native/id goals goalcompile-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常驻内存至少几百MBnative-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.jsconsole.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 JavaScriptJava 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用反射创建BeanMyBatis用动态代理生成Mapper实现Jackson用反射做序列化/反序列化。这些在“无JVM”的原生镜像里默认全部失效。解决方式是通过reflect-config.json、proxy-config.json等配置文件提前注册类信息。Spring Boot 3提供了自动配置机制可以生成大部分reflect-config.json。如果是非Spring项目可以直接用native-image参数native-image -H:ReflectionConfigurationFilesreflect-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构建时要确保所有依赖都正确加入classpathnative-image -cp .;lib/* -jar myapp.jar myapp如果依赖没有完整加进classpathnative-image的静态分析阶段可能无法发现类构建过程会报错而且错误信息往往很模糊。7.6 构建日志与失败分析技巧构建失败时不要只看最后几行错误。native-image构建过程非常长真正的错误通常出现在“Analyzing”阶段和“Building”阶段之间。建议把构建日志重定向到文件方便排查mvn -Pnative clean package build.log 21然后搜索关键字Fatal、Error、Exception。如果错误堆栈里出现了具体类名比如java.lang.ClassNotFoundException: com.example.Xxx那基本就是反射配置问题。7.7 大项目分模块做一个先例验证我有一个很实用的习惯第一次给某个大型项目做native-image适配时先建一个极小的模块只引入该项目最核心的依赖和一段最小化入口代码验证这一批依赖能否通过native-image编译。通过后再逐步扩大范围。这个方法能帮你把复杂问题隔离出来不至于在几百个类里找“哪个类触发了这个反射错误”。8. GraalVM后续还能怎么玩GraalVM的整个生态还在快速迭代几个值得关注的后续方向一是NaCNative Compilation在云原生领域的应用。很多云服务商已经支持基于GraalVM的Spring Boot Serverless部署。二是GraalVM与GraalVM Native Build Tools的持续演进对反射和代理的支持越来越完善尤其是GraalVM 22.2之后新增的Tracing Agent能自动收集运行过程中的反射调用并生成配置文件大幅降低了适配成本。三是GraalPyPython实现、Ruby等语言的逐步成熟。虽然现在标记为实验性但Oracle的投入方向很明确未来在数据科学和AI领域可能会出现更多交叉用法。就我个人而言最直观的体会是以前给客户写工具总得要求对方装JDK、配置JAVA_HOME、弄一堆环境变量现在直接用native-image打一个exe丢过去双击就能跑省掉的不只是沟通成本还有那种“怎么你机器上跑不起来”的挫败感。最后再分享一个小技巧千万别把JAVA_HOME直接指向GraalVM的安装目录之后就把系统里原来的JDK删掉。等你的所有项目都确定能在GraalVM下正常工作、所有依赖库都验证过兼容性之后再清理也不迟。GraalVM虽然很强但它在Windows上的生态位还不至于让传统JDK提前退休两条腿走路永远是更稳的姿势。