
1. 为什么要把 jar 转成 exe还要顺手把 jre 瘦下来做过 Java 桌面工具的人大概都遇到过这个尴尬代码写完了功能也测通了打包成 jar 双击能跑但一旦发给不懂技术的同事或者客户对方第一句话往往是“你这文件怎么打不开”。原因很简单对方机器上根本没装 Java 运行环境。你总不能要求每个使用者都去装一个两百多兆的 JDK 或者 JRE装完还得配环境变量这一步就能劝退一大半人。所以把 jar 转成 exe本质上解决的是分发门槛问题。exe 是 Windows 用户最熟悉的可执行文件形态双击就跑心理负担几乎为零。而 exe4j 就是干这件事的老牌工具之一它能把 jar 包装成一个原生 exe 启动器同时还能把 JRE 一起打包进去做到“绿色免安装”。但问题紧接着就来了一个完整的 JRE 动辄 150MB 到 200MB你辛辛苦苦写的一个小工具可能才几百 KB结果打包出来一个 200MB 的文件夹发给别人都嫌丢人。这时候就需要精简 JRE——把 JRE 里那些你这个项目根本用不到的模块、字体、插件、调试工具统统删掉只留下能让你程序跑起来的最小集合。这篇内容就是围绕这条链路展开的jar 转 exe JRE 精简。我会把 exe4j 的配置要点、JRE 精简的判断逻辑、哪些文件能删哪些绝对不能碰、以及我实际踩过的坑全部摊开讲清楚。适合手里有 Java 桌面项目、需要做绿色分发的开发者也适合刚接触打包、被 JRE 体积吓到的新手。整套流程不需要你懂 C也不需要额外装什么冷门工具跟着做就能复现。2. 整体思路与方案选型为什么是 exe4j 加手工精简 JRE2.1 三条常见路线的对比与取舍把 Java 程序做成 Windows 可执行文件市面上能走的路其实不止一条。我在不同项目里分别试过几种方案这里先把它们摆出来对比一下你就明白为什么我最后选了 exe4j 配合手工精简 JRE 这条路。方案原理优点缺点适用场景exe4j 精简 JRE生成原生启动器调用内置 JRE体积可控、配置直观、兼容性好需要手工精简、仅限 Windows中小型桌面工具、绿色分发打包成单一 fat jar把依赖全塞进一个 jar简单、跨平台仍需目标机装 Java、体积大内部使用、开发者之间传递原生编译方案把字节码提前编译成机器码启动快、无需 JRE配置复杂、反射兼容坑多对启动速度极敏感的场景我选 exe4j 的核心理由有三个。第一它生成的 exe 是一个真正的 Windows 可执行文件用户双击的体验和普通软件完全一致。第二它支持把 JRE 目录一起带上程序启动时优先用自带的 JRE不依赖系统环境。第三它的配置界面虽然选项多但逻辑清晰尤其是 JRE 搜索顺序那块配好一次就能一直用。至于为什么不直接用 fat jar很简单——fat jar 解决的是“依赖打包”问题没解决“运行环境”问题。用户机器上没 Javafat jar 照样打不开。而原生编译方案虽然听起来最优雅但实际项目里只要用到反射、动态代理、序列化这些特性就很容易在编译期报一堆找不到类的错误调试成本太高。对于绝大多数业务型桌面工具来说exe4j 这条路的性价比是最高的。2.2 精简 JRE 的核心逻辑按需保留而非盲目删减很多人一听“精简 JRE”第一反应是去网上找一个“最小 JRE 包”下载下来用。我不推荐这么干原因有两个一是你不知道那个包删了什么万一删掉了你项目依赖的模块运行时才报错排查起来很痛苦二是不同项目依赖的模块不一样别人的最小集合未必适合你。正确的思路是从完整 JRE 出发按你的项目实际用到的能力去裁剪。JRE 的目录结构其实很有规律主要就是这么几块bin可执行文件和 DLL核心是java.exe、javaw.exe以及一堆运行时 DLL。lib核心类库包括rt.jar老版本、modules新版本、charsets.jar、jce.jar等。lib/ext扩展类库一般项目用不到。conf配置文件新版本 JDK 里比较重要。legal、man、include等文档和头文件运行时完全不需要。精简的本质就是保留 bin 里的核心可执行文件和 DLL保留 lib 里你真正用到的模块其余全部删掉。听起来简单但“真正用到”这四个字是难点后面我会讲怎么用工具去判断。2.3 版本选择的一个关键决策这里有个很多人忽略的点你用什么版本的 JDK 编译就尽量用什么版本的 JRE 去打包。我见过有人用 JDK 17 编译然后拿一个 JDK 8 的 JRE 去打包结果运行时直接抛UnsupportedClassVersionError。这不是 exe4j 的问题是字节码版本不兼容。另外从 JDK 9 开始Java 引入了模块化系统JRE 的结构和 JDK 8 时代差别很大。JDK 8 的 JRE 是独立的而 JDK 9 以后官方不再单独提供 JRE你需要用jlink自己生成一个。这就引出了两种精简策略JDK 8 及以前手工删文件配合工具分析依赖。JDK 9 及以后优先用jlink生成定制运行时再手工微调。我下面会两种都讲因为实际工作中这两种环境都会遇到。你可以根据自己的 JDK 版本对号入座。3. exe4j 打包 jar 的核心配置与实操要点3.1 打包前的准备工作清单和依赖一个都不能少在打开 exe4j 之前有几件事必须先确认好否则后面会反复返工。第一确认你的 jar 是可执行的。也就是说META-INF/MANIFEST.MF里必须有Main-Class这一项。如果你是用 Maven 或 Gradle 构建的检查一下打包插件有没有正确写入主类。我遇到过有人直接拿一个普通 jar 去打包结果 exe4j 找不到入口类报错信息还很含糊。第二把所有第三方依赖整理清楚。exe4j 支持两种方式一种是把依赖一起打进你的主 jarfat jar另一种是把依赖 jar 放在一个lib目录里在 exe4j 里配置 classpath。我个人更推荐后者因为依赖和主程序分离后续更新主程序时不用重新打包所有依赖。第三准备一个干净的 JRE 目录。先别急着精简先用完整的 JRE 把整个流程跑通确认 exe 能正常启动、功能正常然后再去做精简。这个顺序很重要否则一旦出问题你分不清是打包配置错了还是 JRE 删多了。第四确认图标文件。exe4j 支持给 exe 设置自定义图标格式必须是.ico。如果你只有一个 png需要先转成 ico。这个细节虽小但直接影响成品的观感。3.2 exe4j 配置流程逐步拆解打开 exe4j 后它会用一个向导式的界面引导你完成配置。我按实际操作的顺序把关键步骤和每个步骤背后的意图讲清楚。第一步选择项目类型。向导第一页会让你选“Regular mode”还是“JAR in EXE mode”。这两个的区别是Regular mode 生成一个独立的 exe你的程序逻辑通过配置的 classpath 加载JAR in EXE mode 则是把 jar 直接嵌入 exe。我一般选 Regular mode因为结构更清晰jar 和 exe 分离方便替换。第二步填写应用信息。这里要填应用名称、输出目录、可执行文件名。输出目录建议单独建一个空文件夹因为 exe4j 会往里面生成 exe 和一堆配置文件。可执行文件名不要用中文也不要有空格避免后续路径解析出问题。第三步配置可执行文件的形态。这里有几个选项值得注意Console application 还是 GUI application如果你的程序有图形界面选 GUI这样启动时不会弹出一个黑色命令行窗口如果是命令行工具选 Console。是否允许单实例如果你的工具不希望被同时打开多个可以勾选单实例选项。图标设置在这里指定你的 ico 文件。第四步配置 JRE 搜索顺序。这是整个流程里最关键的一步。exe4j 允许你指定多个 JRE 搜索路径按顺序查找。我的配置策略是这样的首先搜索 exe 同目录下的jre文件夹也就是我们打包进去的精简 JRE。其次搜索 exe 同目录下的jre子目录的变体。最后才搜索系统环境变量里的 Java。这样配置的好处是优先用自带的 JRE保证运行环境一致万一自带的出了问题还能回退到系统 Java。搜索顺序的配置界面里你可以通过添加“Search sequence”条目来实现每一条可以指定是相对路径还是绝对路径。第五步配置 classpath。把你的主 jar 和所有依赖 jar 都加进来。注意顺序主 jar 一般放在最前面。如果你的依赖很多可以用通配符指定一个目录比如lib/*这样以后加依赖不用改配置。第六步配置主类。填入你程序的入口类全限定名比如com.example.MainApp。exe4j 会自动去 classpath 里找这个类。第七步配置 JVM 参数。这里可以设置初始堆大小、最大堆大小、系统属性等。对于桌面工具我一般设置-Xms64m -Xmx512m既不会一启动就占太多内存也能应付大部分场景。如果你的程序处理大文件可以适当调大。配置完成后点击生成exe4j 会在输出目录里生成 exe 文件和一个jre相关的配置。这时候先别急着精简 JRE把完整的 JRE 复制到 exe 同目录下命名为jre然后双击 exe 测试。3.3 一个容易被忽略的细节相对路径与工作目录exe4j 生成的 exe 在启动时工作目录默认是 exe 所在的目录。这一点很重要因为你的程序里如果有读取配置文件、写日志、加载资源的逻辑用的相对路径都是相对于这个目录的。我踩过一次坑程序里用new File(config/settings.properties)读取配置在 IDE 里跑没问题打包成 exe 后死活读不到。后来发现是工作目录变了。解决办法有两个一是把配置文件的读取改成基于System.getProperty(user.dir)或者 exe 所在目录来拼接绝对路径二是在 exe4j 里配置工作目录。我一般用第一种代码层面解决更可靠。另外如果你的程序需要写文件注意不要往 Program Files 这种需要管理员权限的目录写。绿色软件的最佳实践是所有可写数据都放在 exe 同目录或者用户目录下。4. JRE 精简的完整实操从完整包到最小集合4.1 先搞清楚你的项目到底依赖哪些模块精简 JRE 最大的风险是删多了导致运行时缺类。所以第一步不是删而是分析依赖。这里我推荐一个思路用jdeps工具来分析。jdeps是 JDK 自带的一个依赖分析工具可以扫描你的 jar 包列出它依赖了哪些 JDK 内部模块。命令大概是这样jdeps --multi-release 17 --ignore-missing-deps --print-module-deps your-app.jar这条命令会输出一个模块列表比如java.base,java.desktop,java.logging之类。这个列表就是你jlink生成运行时需要的模块集合。注意--ignore-missing-deps是为了跳过那些第三方依赖找不到的情况因为我们只关心 JDK 模块依赖。如果你用的是 JDK 8没有模块化系统jdeps的输出形式不一样它会列出具体的包依赖。这时候你需要根据包名去判断对应哪些 jar 文件。除了jdeps还有一个更粗暴但有效的办法运行时监控。先把完整 JRE 跑起来用-verbose:class参数启动把加载的类全部打印出来然后看这些类分别来自哪些 jar。这个方法适合依赖关系特别复杂的项目缺点是日志量大需要过滤。4.2 JDK 9 及以上用 jlink 生成定制运行时如果你用的是 JDK 9 或更高版本精简 JRE 最优雅的方式就是用jlink。它本来就是为这个场景设计的。基本命令长这样jlink --module-path %JAVA_HOME%\jmods ^ --add-modules java.base,java.desktop,java.logging ^ --output my-custom-jre ^ --strip-debug ^ --no-header-files ^ --no-man-pages ^ --compress2逐条解释一下这些参数--module-path指向 JDK 的jmods目录里面是各个模块的打包文件。--add-modules指定要包含的模块把jdeps分析出来的列表填进去。--output输出目录。--strip-debug去掉调试信息能省不少体积。--no-header-files不包含 C 头文件运行时用不到。--no-man-pages不包含手册页。--compress2压缩级别2 是最高压缩体积最小但启动稍慢一点点。用这套参数生成的运行时通常能压到 40MB 到 60MB 左右具体取决于你包含了哪些模块。如果只依赖java.base甚至能压到 30MB 以内。生成完之后把这个目录改名为jre放到 exe 同目录下再测试一遍程序。如果一切正常精简就完成了大半。4.3 JDK 8手工删减的取舍与验证JDK 8 没有jlink只能手工删。但手工删也不是瞎删我总结了一套相对安全的流程。第一步备份。把完整的 JRE 复制一份出来命名为jre-full作为备份然后在副本上操作。这一步千万别省删错了还能还原。第二步删除明显不需要的目录。以下这些目录在运行时基本用不到可以直接删lib/ext扩展目录除非你明确用了这里的 jar。lib/missioncontrol、lib/visualvm监控和性能分析工具。bin下的各种工具 exe比如javac.exe、jar.exe、keytool.exe、jconsole.exe等只保留java.exe和javaw.exe。legal、man、include、demo、sample文档和示例。第三步处理rt.jar。这是 JDK 8 里最大的一个文件通常有 60MB 左右。它包含了整个 Java 标准库。理论上你可以用工具把里面用不到的类删掉但我不推荐这么做因为rt.jar是签名的删改后可能导致校验失败。更稳妥的做法是保留完整的rt.jar把精简的重点放在其他文件上。第四步删除多余的字符集和本地化文件。lib下有一堆charsets-*.jar和localedata相关的文件。如果你确定程序只处理中文和英文可以删掉其他语言的数据。但这一步要谨慎删错了可能导致某些编码转换失败。第五步验证。每删一批文件就启动一次程序把主要功能都点一遍。不要一次性全删完再测那样出了问题你根本不知道是哪个文件导致的。我实测下来JDK 8 的 JRE 从 180MB 左右精简到 60MB 到 80MB 是比较稳妥的区间。再往下压风险就开始明显上升了。4.4 精简后的目录结构参考不管用哪种方式精简完成后你的发布目录大概应该是这个结构your-app/ ├── your-app.exe # exe4j 生成的启动器 ├── your-app.jar # 主程序 ├── lib/ # 第三方依赖 jar │ ├── dependency1.jar │ └── dependency2.jar └── jre/ # 精简后的运行时 ├── bin/ │ ├── java.exe │ ├── javaw.exe │ └── *.dll ├── lib/ │ └── ... └── conf/ # 新版本才有这个结构清晰、自包含整个文件夹拷到任何 Windows 机器上都能直接跑不需要安装任何东西。这才是真正的绿色分发。5. 常见问题与排查技巧实录5.1 启动报错速查表下面这张表是我在实际打包过程中遇到过的典型问题以及对应的排查方向。你可以把它当成一个速查手册遇到问题先对号入座。报错现象可能原因排查方向双击 exe 无反应GUI 模式但主类抛异常临时改成 Console 模式看输出提示找不到主类classpath 配置错误检查主 jar 是否在 classpath 里UnsupportedClassVersionErrorJRE 版本低于编译版本统一 JDK 和 JRE 版本NoClassDefFoundError精简 JRE 删多了用完整 JRE 对比测试中文乱码字符集文件被删恢复 charsets.jar 或相关文件启动后闪退JVM 参数配置错误检查 -Xmx 是否超过机器内存找不到配置文件工作目录变化改用绝对路径读取5.2 几个我踩过的坑和独家经验坑一精简 JRE 后程序在开发机上正常在别人机器上报错。这个问题困扰了我很久。后来发现原因是开发机上装了完整的 JDK程序启动时虽然优先用了自带的精简 JRE但某些类加载失败后回退到了系统 JDK。而在别人机器上没有这个回退就直接报错了。解决办法是在一台没有装 Java 的干净机器上做最终测试或者用虚拟机模拟干净环境。这一步绝对不能省。坑二exe4j 的 JRE 搜索顺序配置了但没生效。检查一下你配置的路径是相对路径还是绝对路径。相对路径是相对于 exe 所在目录的如果你写的是jre那 exe 同目录下必须有一个叫jre的文件夹。另外搜索顺序是从上到下第一个找到的就会被使用所以自带 JRE 一定要放在最前面。坑三jlink 生成的运行时缺少字体导致界面文字显示成方块。这个问题在java.desktop模块里比较常见。jlink默认不会包含字体相关的资源。解决办法是在--add-modules里确保包含了java.desktop并且如果还是缺字体可以从完整 JRE 的lib/fonts目录里把字体文件复制过来。坑四打包后的 exe 被杀毒软件误报。exe4j 生成的启动器有时候会被某些杀毒软件标记为可疑。这不是病毒是启动器的行为特征导致的。解决办法有两个一是给 exe 做代码签名需要证书二是把 exe 提交给杀毒厂商做白名单。如果只是内部使用让用户添加信任即可。坑五程序里用了反射jlink 精简后运行时报 ClassNotFoundException。这是jlink的一个已知局限它只能分析静态依赖反射调用的类它分析不到。解决办法是在jlink命令里用--add-modules手动把反射用到的模块加进去或者用jdeps的--generate-module-info选项生成模块描述符后再分析。5.3 精简程度的平衡点怎么找精简 JRE 本质上是在体积和兼容性之间找平衡。我的经验是如果你只是自己用或者小范围分发精简到 60MB 到 80MB 就足够了没必要追求极致。如果要面向大量用户分发可以压到 40MB 左右但一定要做充分的测试。不要为了省那十几兆去删rt.jar里的类风险收益比太低。另外精简完之后建议把整个发布目录打成一个 zip 包记录一下压缩后的体积。通常 60MB 的 JRE 压缩后能到 25MB 左右加上你的程序整体控制在 30MB 以内是完全可行的。6. 发布前的最终检查清单在把成品发给别人之前我习惯过一遍下面这个清单。虽然看起来琐碎但每一条都是血泪教训换来的。在干净环境测试找一台没装 Java 的机器或者用虚拟机完整跑一遍所有功能。检查所有路径确认程序里没有硬编码的开发机路径。确认图标和版本信息exe 的属性里填好版本号、公司名可以用代称看起来更专业。测试中文和特殊字符如果程序涉及文件读写测试一下中文路径和中文文件名。检查日志输出确认日志文件写在可写目录不会因为权限问题失败。压缩包测试把发布目录打成 zip解压到另一个路径再跑一遍确认没有依赖绝对路径的问题。记录 JRE 版本和精简参数把jlink的命令或者手工删除的清单记下来下次打包直接复用。这套流程走下来一个体积可控、双击即用、不依赖系统环境的 Java 桌面工具就成型了。exe4j 负责把入口包装好精简 JRE 负责把体积压下来两者配合基本能覆盖大部分中小型桌面项目的分发需求。我个人在实际操作中的体会是打包这件事最耗时间的不是配置而是排查那些“开发机能跑、别人机器跑不了”的问题。所以每次精简完宁可多花二十分钟在干净环境里测一遍也不要等用户反馈了再回头找原因。另外jlink那套参数建议存成一个脚本文件下次换个项目改改模块列表就能直接用效率能提升不少。