GraalVM实践:在Windows下用Native Image将Java应用打包成exe

发布时间:2026/10/1 13:24:51
GraalVM实践:在Windows下用Native Image将Java应用打包成exe 最近做一个小工具的时候被Java应用的分发问题折腾得够呛——本地跑得好好的发给同事要么让他装JDK要么让他配环境变量稍微老一点的机器还会被启动速度折磨。后来把GraalVM在Windows上完整折腾了一遍把应用直接打成exe双击就能跑问题一下子清爽了很多。这篇文章就把我在Windows下从下载、配置到实际打包的完整过程整理出来包括踩过的几个坑给同样想在Windows下用GraalVM做原生镜像的朋友当个参考。先说明一下这不是一篇只教“下一步下一步”的安装流水账。除了安装步骤我会把GraalVM的核心原理、native-image在Windows上为什么需要额外装Visual Studio构建工具、Spring Boot这种重量级框架怎么迁移以及常见报错怎么排查都摊开讲清楚。你照着做基本能在半小时内跑通第一个原生可执行文件。1. 先搞清楚GraalVM解决了什么问题1.1 JVM和GraalVM的本质区别传统Java应用跑在JVM上执行方式是解释执行加JIT编译。JVM启动时先要加载类、做字节码校验、初始化各种运行时组件然后热点代码再被JIT编译成机器码。这个机制保证了跨平台但也带来了两个老大难问题启动慢、内存占用高。一个简单的Spring Boot应用冷启动少说四五秒堆内存默认就吃几百MB。这在常规服务器上还好一旦到了Serverless、微服务扩容、命令行工具这些场景代价就很明显了。GraalVM的核心卖点是AOTAhead-Of-Time编译。它提供了一种叫native-image的工具可以在构建期就把Java字节码直接编译成可执行文件——在Windows上就是一个exe。这个exe已经包含编译后的机器码运行时不再需要JVM因此启动速度可以做到几十毫秒甚至几毫秒内存占用也大幅下降。打个比方传统JVM像你每次开会都带一个实时翻译GraalVM原生镜像则是提前把演讲稿全部翻好印成资料开会直接发纸质版省掉了现场翻译这个环节。代价是“提前翻译”需要时间而且如果演讲稿里有临时加的段子对应反射、动态代理这类运行时行为你得提前告诉翻译否则现场就翻不出来。1.2 在Windows上使用GraalVM的三个典型场景结合我自己和身边同事的实际使用情况Windows下用GraalVM主要集中在这三类场景场景一把它当普通JDK用。GraalVM本身是一个完整的JDK发行版支持Java标准库日常开发、运行Java程序完全没有问题。如果你只是想要一个更“现代”的JDK可以直接换掉现有的OpenJDK。场景二把命令行工具、内部小工具、Excel处理脚本等打成exe分发。这是我最常用的场景。公司内部很多同事不懂Java也不装JDK你扔一个exe给他双击就能用省去一堆环境问题。场景三Spring Boot应用原生镜像化。针对启动速度和内存敏感的服务把Spring Boot打成原生可执行文件部署时不再需要前置安装JRE容器镜像也能做得更小。当然也要说清楚不是所有Java应用都适合上GraalVM。如果项目重度依赖反射、动态代理、JNI、自定义类加载器或者用了大量第三方库且没有提供native-image支持迁移成本会比较高。所以后面第五章我会专门讲Spring Boot场景下该怎么评估。2. 安装前的版本选择和前置环境准备2.1 GraalVM发行版怎么选目前GraalVM的下载渠道主要有两个Oracle官方和GitHub Releases。版本命名上要注意GraalVM for JDK 21是当前最稳妥的选择Java 21本身就是LTS版本Spring Boot 3.2及以后对它的原生镜像支持也很完善。如果你还在用JDK 17的老项目可以选GraalVM for JDK 17但功能更新和适配力度不如21积极。我直接给出版本选择的建议表使用场景推荐版本说明新项目、Spring Boot 3.xGraalVM for JDK 21LTS社区生态最活跃原生镜像支持最完善老项目、技术栈固定在JDK 17GraalVM for JDK 17兼容已有构建配置迁移成本相对低纯粹体验最新特性GraalVM for JDK 23功能较新但配套生态和插件可能滞后下载时认准Windows x64的zip包比如graalvm-jdk-21.0.2_windows-x64_bin.zip。我个人建议下载zip包自己解压不要用安装器。原因很简单zip包不需要管理员权限解压以后可以随意移动目录还能方便维护多个JDK版本。当然前提是你已经装好了JDK相关环境知道JAVA_HOME和Path这些环境变量是怎么回事。2.2 Windows上必装的前置工具这一步是很多教程没讲透、但恰恰最坑的地方在Windows上使用native-image必须安装Visual Studio Build Tools否则构建必定失败。为什么会这样呢因为native-image在把Java字节码编译成机器码之后还需要执行链接操作生成一个Windows下的PE格式可执行文件。这个链接过程依赖MSVC的link.exe还有Windows SDK里的各种系统库。没有它哪怕你编译阶段OK最后一步也会报“找不到link.exe”或者“MSVC not found”。安装方法去Visual Studio官网下载Build Tools运行安装器以后勾选“使用C的桌面开发”工作负载。默认会包含MSVC v143编译器、Windows 11/10 SDK、CMake工具集等组件。这一步比较大可能要下载好几个GB建议在网速好的时候操作。安装完成以后注意一个关键细节日常的CMD或者PowerShell窗口不会自动加载MSVC的环境变量。你需要从开始菜单找到“x64 Native Tools Command Prompt for VS 2022”在这个终端里执行native-image相关命令才能找到C编译器。这个细节我后面实操章节还会再强调一次。3. Windows详细安装步骤3.1 下载、解压和目录规划下载好zip包后先规划目录。我建议放在D:\dev\graalvm-jdk-21.0.213.1这种路径注意两点路径不要太深、不要有中文和空格。虽然现代Windows对中英文路径支持已经很成熟但是native-image在扫描类路径和资源文件时遇到中文路径出现过诡异的构建失败为了少踩坑尽量用纯英文路径。解压后目录结构大概是这样bin目录下有java.exe、javac.exe、native-image.cmd等可执行文件。如果你用的是GraalVM for JDK 21及之后的版本native-image已经默认内置在发行版中不需要额外安装。如果是GraalVM for JDK 17这类稍老一点的发行版可能还需要用gu install native-image手动安装组件。判断方法很简单打开bin目录看有没有native-image.cmd文件。3.2 配置环境变量这一步是安装的关键。右键“此电脑” - “属性” - “高级系统设置” - “环境变量”在系统变量里操作新建变量JAVA_HOME值为GraalVM的解压路径比如D:\dev\graalvm-jdk-21.0.213.1。编辑Path变量在开头新增一行%JAVA_HOME%\bin。放到开头是为了避免系统里还有其他JDK时命令解析到旧版本。可选但推荐新建变量GRAALVM_HOME值同JAVA_HOME。某些构建工具和IDE会读取这个变量。配置好以后重新打开一个CMD窗口依次执行java -version javac -version where java如果java -version输出里能看到GraalVM相关字样比如GraalVM Community JDK 21.0.2说明环境变量配置成功。这里有个经验之谈配置完环境变量后一定要重新打开终端窗口否则不会生效。另外where java输出的第一条路径应该指向GraalVM的bin目录如果有其他路径排在前面就把Path变量里GraalVM那一项提到最前。3.3 验证native-image工具在CMD窗口运行native-image --version正常会输出native-image的版本信息。如果提示“无法识别native-image”先检查bin目录里是否有native-image.cmd。如果缺失旧版本需要用GraalVM自带的gu工具安装命令是gu install native-image新版本则建议重新下载对应版本的GraalVM因为旧升级通道已不再是主流方式。到这里GraalVM本体已经装好了。但你如果现在就直接跑native-image大概率会在链接阶段报错原因就是上一章提到的MSVC环境问题。所以下一步请打开“x64 Native Tools Command Prompt for VS 2022”在这个终端里重新执行native-image --version确认能读到C构建环境。只要这一步没问题后面打包exe的链路就通了。4. 第一个原生镜像从Java源码到exe4.1 写一个HelloWorld并打包装好环境以后拿一个最小案例跑通全流程建立信心很重要。我先在D:\demo目录下新建一个Hello.javapublic class Hello { public static void main(String[] args) { System.out.println(Hello GraalVM on Windows); } }步骤很简单javac Hello.java native-image Hello如果一切顺利目录下会生成Hello.exe。这个.exe只有几MB直接双击或者在命令行运行屏幕上会立刻打出Hello GraalVM on Windows。你甚至可以把这个exe复制到一台完全没有安装JDK的Windows机器上跑这就是原生镜像最大的价值——免运行时依赖。我第一次跑通这个流程的时候最大的感触是菜鸡如我也能“编译”出原生可执行文件了而且整个过程比想象中顺畅前提是前面的环境准备做扎实了。4.2 native-image常用参数说明实际项目里没有哪个应用是光一个HelloWorld那么简单所以native-image的参数需要了解一些。构造native-image命令行时常见参数如下-o 名称指定输出文件名默认和主类名一致。--no-fallback禁止生成回退镜像。如果不加某些无法分析到位的代码会让exe在运行时动态加载JVM这样便失去了“原生”的意义。建议默认加上。-O2更激进的优化等级打包时间会变长但运行性能更好。--enable-url-protocolshttp,https允许程序在运行时发起HTTP请求。注意原生镜像不会自动包含所有协议处理器需要显式声明。-H:ReportExceptionStackTraces生成更详细的异常堆栈排查运行时问题很有用。-J-Xmx4g设置构建阶段JVM的最大堆内存。构建原生镜像本身是一个耗费内存的过程如果机器内存紧张可以用这个参数控制上限。这里要特别提醒一个Windows相关的坑不要试图直接加--static参数想在Windows上做全静态链接。--static在Linux上确实能生成完全静态的可执行文件但Windows上因为系统库和MSVC的限制通常做不到完全静态化硬加参数反而可能报错。Windows下生成的原生exe在目标机上运行时一般要求系统里有对应的Visual C运行库大多数机器默认都有但如果目标机太精简可能需要装VC Redistributable。4.3 用Maven项目编译成exe实际开发中很少直接裸写命令更多是通过Maven或Gradle来构建。这里给一个最简的Maven接入方式在项目的pom.xml里增加exec-maven-plugin或GraalVM官方构建插件。以官方插件为例plugin groupIdorg.graalvm.buildtools/groupId artifactIdnative-maven-plugin/artifactId version0.10.3/version executions execution idbuild-native/id goals goalcompile-no-fork/goal /goals /execution /executions /plugin然后在“x64 Native Tools Command Prompt for VS 2022”里执行mvn -Pnative native:compile构建完成后target目录下会生成对应的exe文件。这种方式在单体应用或普通Java库项目中已经够用了。5. Spring Boot应用如何迁移到GraalVM原生镜像5.1 不是替换插件而是增加一个构建插件热搜词里有一条“spring boot打包插件可以换成graalvm?”这个描述其实不太准确。Spring Boot项目里spring-boot-maven-plugin负责常规的jar包打包它和GraalVM的构建插件并不是二选一的关系。正确的做法是保留原有的spring-boot-maven-plugin同时增加native-maven-plugin通过Maven的Profile机制区分“普通打包”和“原生打包”两条链路。也就是说普通构建走mvn package生成可执行jar原生构建走mvn -Pnative native:compile生成原生可执行文件。两条路径互不干扰日常开发调试照旧用JVM模式发版或者部署需要快速启动时再用原生模式。5.2 pom.xml配置实战以一个Spring Boot 3.x的Web项目为例最小配置是这样的。父工程确保是spring-boot-starter-parent版本建议3.2以上然后增加依赖和插件parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin plugin groupIdorg.graalvm.buildtools/groupId artifactIdnative-maven-plugin/artifactId /plugin /plugins /build注意native-maven-plugin的版本号一般不用显式指定因为spring-boot-starter-parent的依赖管理已经帮你管好了。然后在“x64 Native Tools Command Prompt for VS 2022”里执行mvn -Pnative native:compile第一次构建会很慢期间会看到GraalVM Native Image执行各类可达性分析、AOT处理Spring相关注解生成target/demo.exe。我实测过一个最简单的Spring Web应用打包时间在两三分钟左右生成的exe启动时间大概在0.3秒级别比jar包启动快了十倍以上。5.3 处理反射、序列化和动态代理Spring Boot本身在启动阶段大量使用反射、CGLIB动态代理和ASM字节码操作这些在JVM模式下没问题但在AOT编译时需要提前收集元信息。如果你只是用Spring MVC、Spring Data JPA这些基础组件插件会自动处理大部分情况但遇上以下场景就需要手工干预使用Gson、Jackson反序列化未直接引用的类使用Hibernate/JPA的实体映射使用Apache HttpClient、Elasticsearch客户端等依赖动态代码生成的框架业务代码里通过Class.forName(...)或ServiceLoader加载类。解决方案分为两步。第一步在代码上使用Spring的RegisterReflectionForBinding注解把需要反射访问的类显式注册例如Configuration RegisterReflectionForBinding(MyRequestDto.class) public class AppConfig { }第二步如果第三方库缺少GraalVM支持元数据可以在src/main/resources/META-INF/native-image/下创建reflect-config.json、resource-config.json等配置文件。不过现在GraalVM社区有专门的graalvm-reachability-metadata仓库很多常用库的元数据已经整理好了。在Maven中引入dependency groupIdorg.graalvm.buildtools/groupId artifactIdgraalvm-reachability-metadata/artifactId version2.3.2/version classifierrepository/classifier scopeprovided/scope /dependency它会自动在构建时补充第三方库的配置信息能省掉大量手工配置。5.4 什么情况下不建议用Spring Boot原生镜像虽然GraalVM原生镜像看起来很香但也要泼一盆冷水。如果项目是几十个模块的大单体构建时间可能长达十几分钟构建机内存要求至少8GB起步这对团队基础设施有要求。另外一些依赖JMX、动态脚本、热部署的开发体验在原生镜像下是不支持的迭代开发还是得靠JVM模式。我的建议是新项目或者对启动时间、内存有硬指标的项目可以用GraalVM原生镜像跑通PoC验证存量老项目不要轻易迁移除非你有大量时间补配置和踩坑。选择技术方案的根本原则永远是“够用就好”不是为了用新技术而用。6. 常见问题与排查技巧实录6.1 构建失败找不到link.exe或MSVC相关错误这是Windows上最经典的问题。报错信息通常会包含Could not find link.exe、MSVC not found或cl.exe not found等关键字。排查思路确认已经安装了Visual Studio Build Tools并且勾选了“使用C的桌面开发”确认你运行native-image或Maven命令的终端是“x64 Native Tools Command Prompt for VS 2022”或已经执行了vcvars64.bat的CMD窗口。我见过不少人是直接在普通PowerShell里跑命令即使VS装好了也会报错属于环境变量没加载的问题。6.2 构建时内存溢出或长时间卡住native-image构建时的内存使用率比较高如果机器的物理内存只有8GB同时开着IDE、浏览器很可能会因内存不足导致构建失败。可以给native-image的构建JVM设置上限native-image -J-Xmx4g -J-Xms1g Hello如果是Maven项目可以设置MAVEN_OPTS-Xmx4g来增大Maven进程堆内存。另外卡在某个进度百分比长时间不动不一定是死机尤其是大项目AOT分析阶段本身很耗时建议保留足够耐心同时留意CPU占用有没有波动。6.3 生成的exe在别的机器上运行报缺少DLL这里要分清两种情况。如果exe在构建机器上运行正常换一台机器就报缺少vcruntime140.dll或msvcp140.dll说明目标机器缺少Visual C Redistributable。解决办法是在目标机器上安装对应版本的VC运行库或者在构建时注意MSVC版本兼容性。如果报错是找不到JNI_CreateJavaVM之类的符号那多半是打出来的不是真正的原生可执行文件而是回退到JVM模式的镜像。仔细检查构建日志里是不是出现了Please use -H:-CheckToolchain或者fallback相关字样有的话加上--no-fallback参数重新构建。6.4 中文路径和空格引发的问题这个问题我在前面提过但值得再强调一次。项目的源码路径、Maven本地仓库路径、GraalVM安装路径这三处尽量不要有中文和空格。如果已经中招表现为构建时报“Unable to locate symbol”或者一堆莫名其妙的类加载错误先把工程复制到纯英文路径下再试一次大多数情况下问题都会消失。6.5 Spring Boot打包后运行时反射异常如果exe能启动但运行到某个业务时抛ClassNotFoundException、NoSuchMethodException或者ReflectionException基本都是反射元数据缺失。解决办法是找到报错对应的类用RegisterReflectionForBinding注册或者补充到reflect-config.json里。建议在pom里把graalvm-reachability-metadata依赖加上能一次性解决很多第三方库的元数据问题。6.6 常见问题速查表现象常见原因处理办法native-image --version报系统找不到文件未安装组件或版本过旧确认bin目录有native-image.cmd否则换用GraalVM for JDK 21完整版本构建时报link.exe/MSVC找不到缺少VS Build Tools或未使用对应终端安装C桌面开发负载在x64 Native Tools终端运行打包过程被杀或内存溢出构建内存不够设置-J-Xmx4g关闭占用内存的软件生成的exe在别的机器缺少DLL目标机缺少VC运行库安装Visual C RedistributableSpring Boot启动后反射类报错缺少GraalVM反射配置加RegisterReflectionForBinding或引入reachability元数据有中文路径时构建失败工具链对Unicode路径支持不稳全部移动到纯英文路径6.7 判断是否构建成功的几个经验信号构建成功的日志末尾会出现Build completed successfully并且会显示生成文件的大小和耗时。构建失败的日志通常会在中间段给出红色或ERROR级别的信息。我个人习惯是把构建日志保存起来遇到问题先在日志里搜索error、Caused by、Fatal这几个关键词能快速定位到真正的错误位置不被前面大量的提示信息干扰。最后再分享一个实际操作中的体会GraalVM在Windows上最让我满意的地方不是跑大型Spring Boot应用而是公司内部那些几十行到几百行的自动化小工具。以前发给别人用要么让人装JDK要么用脚本包一层JRE麻烦得很。现在直接native-image打成exe丢给对方双击就行省事太多。如果你也在被Java应用分发问题困扰不妨先从一个小工具开始体验跑通以后你自然会判断它在你项目里的适用边界。