为什么 JDK 的镜像比 JRE 镜像小?
| docker aiauthor
TLDR:虽然 JDK 镜像内容比 JRE 镜像多,但 JDK 镜像的压缩比更高,导致最后镜像大小反而 JDK 比 JRE 小;原因是 JDK 镜像主要用于 CI/CD 流水线,对性能和耗时要求不太高,所以可以用更高的压缩比;但 JRE 镜像主要用于线上服务,对性能要求极高,因此基本没有压缩。
AI 贡献提示:技术探索过程主要由 Claude Code + Kimi K2 完成;文字部分主要为 Gemini Pro 2.5 编写;我仅提供探索方向指导和简单内容编辑。我已在能力范围内确认下述内容的准确性。如发现问题,欢迎留言反馈。
感谢 @ziqin 提出了本文的探索问题。
缘起:一个反常的发现
一位群友在运行 docker image 时发现一个反常的现象:JDK镜像(111MB)竟然比JRE镜像(139MB)小了整整28MB!
REPOSITORY TAG IMAGE ID CREATED SIZE
bellsoft/liberica-runtime-container jdk-21-musl 1c9d58aebbdc 2 days ago 111MB
bellsoft/liberica-runtime-container jre-21-musl c59d660b1633 2 days ago 139MB
但这感觉上不太合理,因为 JDK 是 JRE 的超集,不仅包含了 JRE 的全部功能,还包含了额外的开发工具(如 javac
, jdb
等),为什么其镜像反倒还更小呢?这个反直觉的观察结果立刻激起了我们的好奇心。问题提出之时是个工作日的晚上,我并没有太多精力仔细思考,于是我让 Claude Code 来探索这个问题。
探案之旅:层层深入,拨开迷雾
我们的调查遵循着从宏观到微观的路径,一步步逼近问题的核心。
第一站:文件系统对比
我们首先通过 docker exec
进入两个正在运行的容器,对比其内部文件系统的差异。
- JDK 镜像:
docker exec jdk-analysis du -sh /usr/lib/jvm/liberica21-lite
-> 98.5M - JRE 镜像:
docker exec jre-analysis du -sh /usr/lib/jvm/liberica21-container-jre
-> 125.2M
初步结论:差异的根源在于Java安装目录本身。JDK镜像使用的是一个名为 liberica21-lite
的发行版,而JRE镜像使用的是 liberica21-container-jre
。
第二站:核心文件对比
当我们继续深入,对比两者 lib
目录下的核心文件 modules
时,差异变得更加惊人:
- JDK
modules
文件: 59.3MB - JRE
modules
文件: 97.1MB
modules
文件是Java模块化系统的核心,存储了所有的运行时模块。JRE的modules
文件竟然比JDK的大了37.8MB! 这几乎完全解释了镜像大小的差异。
但新的问题随之而来:JDK明明包含了更多的模块(如编译器 jdk.compiler
、文档工具 jdk.javadoc
等),为什么它的 modules
文件反而更小?
对比项 | JDK (liberica21-lite) | JRE (liberica21-container-jre) | 差异 |
---|---|---|---|
模块数量 | 69 个 | 49 个 | +20 个 |
modules 文件大小 |
59.3 MB | 97.1 MB | -37.8 MB |
更多的内容,却占用了更少的空间。这背后一定有更深层次的原因。
第三站:压缩策略对比
为了彻底搞清楚 modules
文件内部的秘密,我们使用 jimage
工具将其解压,并分析了其中包含的每一个资源。真相终于水落石出。
这并非内容差异,而是压缩策略的根本不同!
观察以下对比数据:
指标 | JDK (liberica21-lite) | JRE (liberica21-container-jre) | 证据 |
---|---|---|---|
压缩算法 | DEFLATE (zlib) | DEFLATE (zlib) | jimage 工具确认 |
压缩级别 | Level 9 (最大) | Level 0 (无) | 资源分析确认 |
总资源数 | 28,427 | 22,133 | jimage list --verbose |
压缩资源数 | 28,044 (98.7%) | 0 (0.0%) | |
未压缩资源数 | 383 (1.3%) | 22,133 (100.0%) | JRE裸奔存储 |
压缩后总大小 | 60.4 MB | 100.5 MB | JRE因元数据开销反而变大 |
压缩比 | 2.31 : 1 | 0.99 : 1 | JRE压缩比小于1 |
真正的技术根因:
- JDK (
liberica21-lite
): 使用了激进的DEFLATE压缩(相当于jlink --compress=2
,级别9)。它将所有工具和资源(包括开发工具、调试信息、所有区域设置)都包含进来,然后用最高效的算法进行压缩,以实现最小的磁盘占用。 - JRE (
liberica21-container-jre
): 完全没有使用压缩(相当于jlink --compress=0
,级别0/STORE模式)。它精心挑选了生产环境必需的运行时子集,但为了追求最快的启动速度和运行时性能,放弃了压缩。(甚至因为压缩元数据,反而引入了额外的空间开销,导致压缩比小于 1。)
设计哲学:为何如此选择?
这个看似矛盾的设计,实际上是BellSoft针对不同应用场景的深思熟虑的工程决策。
JDK “lite” 的设计目标 (--compress=2
)
- 目标场景: CI/CD流水线、开发环境、容器构建阶段。
- 优化核心: 存储和网络效率。在这些场景下,镜像的下载速度和存储成本是首要考虑因素。构建时的一次性压缩CPU开销,可以换来后续无数次快速的分发和部署。
- 策略: 空间换时间(构建时)。牺牲构建时的CPU时间,换取最小的存储空间。
JRE “container-jre” 的设计目标 (--compress=0
)
- 目标场景: 生产环境运行时。
- 优化核心: 运行时性能。在生产环境中,应用的启动速度、内存占用和CPU效率至关重要。免去解压步骤,意味着更快的类加载、更低的CPU消耗和更少的内存抖动。
- 策略: 时间换空间(运行时)。牺牲磁盘空间,换取运行时的高性能和稳定性。
最终结论:一个反直觉的真理
我们最初的谜题现在有了清晰的答案:
JDK镜像小,是因为它用极致的压缩,换取了分发和存储的便利。 JRE镜像大,是因为它用空间,换取了生产环境的极致性能。
换句话说:
- 更多内容 + 更强压缩 = 更小的分发体积 (JDK)
- 更少内容 + 无压缩 = 更大的分发体积 (JRE)
结语
这次从一个简单的 docker images
命令开始的探案之旅,最终带领我们深入理解了现代 Java 发行版在容器化时代的精妙设计。BellSoft Liberica 的这种差异化策略,并非一个错误,而是一个深刻理解开发者和运维者在不同阶段核心痛点的高级功能。
它告诉我们,在技术的选择上,没有绝对的“好”与“坏”,只有是否“适合”。理解了这些选择背后的逻辑,我们才能在自己的工作中,做出更明智、更高效的决策。