Java jlink命令:裁剪 JDK,生成定制精简 JRE(裁剪 JVM 镜像)

介绍

jlink(Java Linker),JDK9 引入(JEP 282),属于Java模块化系统(Project Jigsaw)配套工具,用来把一组模块及其传递依赖打包、优化,生成自定义精简运行时镜像(custom runtime image)。

一句话解释

不再分发完整JRE/JDK,只打包你的程序真正需要的Java模块,大幅缩小体积。

背景

  • JDK8及之前:发布Java程序,要么要求客户本机装JRE,要么打包完整JRE(体积很大,几百MB)。
  • JDK9+模块化:Java本身拆成大量独立模块(java.base、java.net等),jlink做链接阶段(介于编译期、运行期之间),自动分析依赖,只包含用到的模块。

能力

  1. 自动依赖分析:给定模块,自动递归引入它依赖的所有模块
  2. 裁剪JRE:剔除未使用模块,产出最小运行环境
  3. 内置优化插件:压缩、删除调试信息、strip符号等进一步瘦身
  4. 输出独立镜像目录:镜像自带bin/java,目标机器不需要预装JDK/JRE,直接运行
  5. 插件机制,支持扩展自定义处理逻辑

⚠️ 限制:原生只支持模块化应用(带module-info.java);普通classpath非模块化项目,不能直接jlink,需要借助第三方工具(如jpackage、spring-boot-jlink、moditect)。

使用场景

  1. 桌面客户端、嵌入式设备、容器镜像:打包独立Java程序,交付包体积显著降低
  2. 云原生轻量镜像:构建最小Java基础镜像,减少镜像大小、攻击面
  3. 分发工具类小程序:客户不需要安装Java环境,解压即用

jlink 打包两种模式

当 jar 参与 jlink 链接(放到 --module-path):
jlink 会把这个模块里面所有的 .class、资源文件,抽取出来,打包进 runtime/lib/modules 这个单一的二进制映像文件里面。
打包完成之后,你在输出目录再也看不到原来的 xxx.jar 文件了。类文件被合并、压缩到 JVM 的内部模块镜像里。

modules 不是 zip/jar,是 JLink 专用的二进制容器,是JVM启动时直接映射读取的。

执行完 jlink 输出 runtime:

runtime/
├─ bin/
│  └─ java.exe
└─ lib/
   ├─ modules      ← 【所有JDK模块 + 你的第三方模块的类全部塞在这里】
   └─ ...其他配置

✅ 原来的 ofdrw-converter.jar、flatlaf.jar 消失了。
所有class都被打进 modules。启动方式:java -m 模块名/主类。
JVM 不再去读独立jar,直接从 modules 里面加载类。

jlink只裁剪JDK,之后手动复制jar进去:

runtime/
├─ bin/
│  └─ java.exe
├─ lib/
│  └─ modules      ← 这里**只有裁剪后的JDK内置模块**
└─ app/
   ├─ ofdrw-converter.jar
   ├─ aspose-pdf.jar
   ├─ flatlaf.jar
   └─ your-app.jar

lib/modules 里面只有JDK的类。
你的所有第三方jar还在 app/ 目录,是独立文件。启动用 java -cp app/* 主类,JVM 从这些独立jar读取类。

关键细节

  1. jlink 的 modules 文件是只读的运行时映像,不是普通压缩包,不能直接解压。
  2. 好处:
    • 减少文件数量;
    • JVM启动加载更快;
    • 理论上可以做死代码消除(有限);
  3. 代价:
    • 所有模块必须是显式模块(带 module-info.class);
    • 一旦打包,想替换其中一个jar,必须重新完整跑一遍jlink;不能单独替换某个jar。

基础语法

jlink [选项] --module-path <模块路径> --add-modules <模块名> --output <输出目录>
  • --module-path:模块查找路径;JDK内置模块在$JAVA_HOME/jmods;自己编译的模块放在mlib
  • --add-modules:入口模块,会自动带上全部传递依赖
  • --output:生成自定义运行时的文件夹
  • 常用可选参数
    • --compress=2:压缩级别(0无压缩,1常量字符串,2全部)
    • --strip-debug:移除调试信息,减小体积
    • --no-header-files:不包含头文件
    • --no-man-pages:不包含man手册

例子:精简运行环境

jlink --module-path "%JAVA_HOME%\jmods" ^
--add-modules java.base,java.desktop,java.xml,java.logging,jdk.crypto.ec,jdk.unsupported ^
--output runtime ^
--strip-debug ^
--compress 2 ^
--no-man-pages ^
--no-header-files

解释

1. --module-path "%JAVA_HOME%\jmods"

模块搜索路径。
%JAVA_HOME%\jmods 目录存放 JDK 内置所有模块(.jmod 文件),告诉 jlink 去哪里读取 JDK 模块。

2. --add-modules 模块列表

指定要打包进精简 JRE 的模块,会连带打包它们的传递依赖

  • java.base:所有 Java 程序的基础核心模块,必选。所有类、基础 IO、基础集合都在这里。
  • java.desktop:聚合模块,一次性带入 AWT / Swing / ImageIO / 字体 / 打印,你的 GUI 窗口、图片渲染依赖它。
  • java.xml:XML 解析,OFD 文件本质是 XML,解析 OFD 需要。
  • java.logging:Java 内置日志模块。
  • jdk.crypto.ec:椭圆曲线加密。OFD/PDF 的签名、证书校验会用到。
  • jdk.unsupported:保留sun.misc.Unsafe等 JDK8 遗留内部 API。Aspose、OFDRW 这类文档库强依赖这个模块,缺少会运行时报错。

注意:--add-modules 会自动递归引入这些模块依赖的其他模块。

3. --output runtime

输出目录,精简后的 JRE 会全部生成到 runtime 文件夹。

⚠️ 重要:执行这条命令前,必须手动删除旧的 runtime 文件夹,否则旧文件残留,结果异常。

4. --strip-debug

剥离调试信息(调试符号、行号、局部变量信息)。

  • 减小lib/modules体积
  • 不影响正常业务运行;出错时异常堆栈还能看到类名方法名,只是丢失源码行号。

5. --compress 2

对lib/modules开启最高级 ZIP 压缩。

  • 0:不压缩;1:字符串共享;2:Deflate 压缩,文件最小
  • 仅压缩 lib/modules(存放 Java class);bin 目录的 dll、exe 完全不压缩
  • 代价:类首次加载需要微小解压开销,GUI 程序几乎感知不到。

6. --no-man-pages

不复制 man 手册(Windows 没用,直接跳过,减少体积)。

7. --no-header-files

不复制 C 语言头文件(开发 JNI 才需要,普通 Java 运行环境不需要,删掉减少体积)。

删除无用的可执行文件

只保留java、javac两个可执行文件,其余全删除:

echo 删除不需要的工具exe
del /Q runtime\bin\jpackage.exe
del /Q runtime\bin\jlink.exe
del /Q runtime\bin\jmod.exe
del /Q runtime\bin\jdeps.exe
del /Q runtime\bin\javap.exe
del /Q runtime\bin\javadoc.exe
del /Q runtime\bin\jar.exe
del /Q runtime\bin\keytool.exe
del /Q runtime\bin\rmiregistry.exe
del /Q runtime\bin\image.exe
del /Q runtime\bin\jfr.exe
del /Q runtime\bin\jdeprscan.exe
del /Q runtime\bin\jarsigner.exe
del /Q runtime\bin\javac.exe
del /Q runtime\bin\serialver.exe
del /Q runtime\bin\jaccesswalker.exe
del /Q runtime\bin\jaccessinspector.exe
del /Q runtime\bin\jabswitch.exe
del /Q runtime\bin\ktab.exe
del /Q runtime\bin\klist.exe
del /Q runtime\bin\kinit.exe

运行程序

裁剪 JDK,生成定制精简 JRE,所以要启动程序测试,防止一些需要的模块被裁剪了:

runtime/bin/java -jar app.jar

jlink vs jpackage

  • jlink:只生成自定义运行时目录,不含打包成exe/msi/dmg安装包
  • jpackage(JDK14+):底层调用jlink,在精简runtime基础上,打包成平台原生安装程序(Windows exe、mac dmg、Linux deb/rpm)

缺点与注意事项

  1. 生成的runtime平台绑定:Windows下jlink只能产出Windows镜像,不能跨平台
  2. 自定义运行时没有自动更新机制,版本漏洞需要重新打包分发
  3. 非模块化项目不能直接jlink;SpringBoot这类传统工程需要额外工具转成模块
  4. 依赖native库时需要手动处理,jlink不会自动识别第三方本地库