必须保留java.base,否则jlink失败;还需按需添加java.desktop、java.logging、jdk.crypto.cryptoki、java.naming等模块,运行报错后用jdeps反查缺失模块。

用 jlink 生成最小 JRE 时,哪些模块必须保留
不加筛选直接 jlink 会失败,因为 Java 9+ 的模块系统默认不包含运行时必需的底层模块。最常漏掉的是 java.base——它不是可选的,是强制依赖,所有自定义 JRE 都得显式包含它。
其他高频需要的手动添加模块:
-
java.desktop:用了 Swing/AWT 或图像处理(如ImageIO)就必须带上 -
java.logging:哪怕只调用了java.util.logging.Logger,缺了就抛NoClassDefFoundError -
jdk.crypto.cryptoki:涉及 TLS 连接(比如 HTTPS 请求)时,某些 Linux 环境下会因缺少 PKCS#11 支持而握手失败 -
java.naming:用了 JNDI(如 Spring Boot 的配置绑定)就得加
判断依据很简单:运行你的应用,看启动时报什么 NoClassDefFoundError 或 ClassNotFoundException,再反查对应类属于哪个模块(可用 jdeps --list-deps your.jar 辅助)。
jlink 命令里 --add-modules 和 --include-locales 怎么配才不踩坑
--add-modules 不接受通配符,也不能写成 java.*。必须逐个列出模块名,或用 ALL-SYSTEM(但那就失去精简意义了)。更稳妥的做法是先用 jdeps --print-module-deps 扫描 jar 包,再把输出结果直接喂给 --add-modules。
立即学习“Java免费学习笔记(深入)”;
--include-locales 容易被忽略——一旦应用里调用了 NumberFormat.getInstance(Locale.CHINA) 或类似本地化 API,又没加这个参数,就会在目标机器上 fallback 到 en_US,甚至抛 MissingResourceException。建议明确写成 --include-locales=zh_CN,en_US,别依赖默认值。
顺带一提:--compress=2 能把模块 JAR 解压后做字节码级压缩,体积通常能再减 15%~20%,但首次启动会略慢一点,权衡取舍看场景。
生成的精简 JRE 在 Docker 和 Windows 上常见启动失败原因
Docker 场景下最典型的问题是:镜像用了 alpine 基础镜像,但 jlink 生成的 JRE 依赖 glibc,直接运行报 not found。解决方法只有两个:FROM openjdk:17-jre-slim 这类 Debian/Ubuntu 基础镜像,或者换用 musl 版 JDK(如 Liberica JDK 的 liberica-openjre-alpine)重新 jlink。
Windows 上容易出问题的是路径权限和符号链接。jlink 在 Windows 下生成的 bin\java.exe 实际是个重定向器(指向 ..\lib\jspawnhelper),如果打包时用了不保留符号链接的工具(比如某些 zip 库),就会变成普通空文件,一运行就闪退。验证方式:进生成目录执行 bin\java -version,不报错才算真正可用。
另外,jlink 默认不打包 JVM 参数文件(conf/jvm.cfg),某些老版本 JDK 构建的 native 启动器会因此找不到 VM,得手动从完整 JDK 里复制一份过去。
如何验证精简 JRE 真的能跑通你的应用
别只测 java -version。真实验证要分三步走:
- 用生成的
bin/java直接运行你的 jar:./jre/bin/java -jar app.jar,观察是否正常启动、HTTP 能否响应、日志是否输出 - 检查进程实际加载的模块:
./jre/bin/java --list-modules | grep -E '^(java|jdk)',确认关键模块都在,且没有意外混入多余模块(比如本不需要java.se却出现了) - 在目标环境(尤其是低配容器或老旧 Windows)上跑一次完整业务链路,比如触发一次数据库查询 + 文件导出 + 发邮件——很多问题只在特定 IO 或线程路径下暴露
最隐蔽的坑是动态类加载:有些框架(如 OSGi、Quarkus Dev Mode)会在运行时反射加载模块,jlink 静态分析根本捕获不到,只能靠实测。这时候宁可多加几个疑似模块,也别为省几 MB 冒险。










