【JVM原理详解】04-JDK与JRE与JVM关系辨析
JDK与JRE与JVM关系辨析
引言
“装JDK还是JRE?”“JDK和JVM到底什么关系?”“为什么JDK 9之后没有单独的JRE下载了?”——这些问题困扰着许多从入门到进阶的Java开发者。前三篇文章你已经了解了JVM如何执行字节码、发展出多样的实现,以及内部三大子系统的分工。本篇将从"工具链"和"部署环境"的视角,帮你彻底理清JDK、JRE、JVM三者的包含关系和职责分工,同时深入探讨JDK 9模块化带来的革命性变化,以及在实际工作中如何选择合适的JDK发行版。
JDK、JRE、JVM的包含关系
三者的关系可以用一句话概括:JDK包含JRE,JRE包含JVM。但这个表述过于简略,容易造成理解偏差。更精确的描述是:
┌─────────────────────────────────────────────────────────┐ │ JDK │ │ (Java Development Kit - Java开发工具包) │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 开发工具 (Development Tools) │ │ │ │ javac, java, javap, javadoc, jar, │ │ │ │ jdb, jconsole, jshell (≥9), jlink (≥9) │ │ │ │ jstat, jmap, jstack, jcmd, jinfo │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────▼──────────────────────┐ │ │ │ JRE │ │ │ │ (Java Runtime Environment - 运行时环境) │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ 运行时库 (Runtime Libraries) │ │ │ │ │ │ rt.jar / java.base 等模块 │ │ │ │ │ │ Java核心类库、资源文件 │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ┌───────────────────▼─────────────────┐ │ │ │ │ │ JVM │ │ │ │ │ │ (Java Virtual Machine - 虚拟机) │ │ │ │ │ │ │ │ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ │ │ 类加载器 │ 运行时数据区 │ │ │ │ │ │ │ │ │ 执行引擎 + GC │ │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ 其他运行时组件 │ │ │ │ │ │ 平台库(如字体、音频) │ │ │ │ │ │ 部署技术(Java Web Start等,已弃用) │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ 辅助资源 (Auxiliary Resources) │ │ │ │ include/ (C头文件用于JNI) │ │ │ │ jmods/ (JDK模块文件, ≥9) │ │ │ │ legal/ (许可证文件) │ │ │ │ conf/ (JVM和JDK配置) │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘JVM是最底层的基础。它只负责执行字节码——加载类、管理内存、执行指令、回收垃圾。JVM本身不包含Java标准类库。理论上你可以在一个只有JVM的裸机上执行字节码,但你会发现连System.out.println都无法使用,因为这个类存在于标准库中,而不是JVM中。
JRE在JVM的基础上增加了Java标准类库(如java.lang、java.util、java.io等)以及其他运行时需要的资源文件。JRE是"可以运行Java程序的最小环境"。它的定位是部署Java应用程序——用户只需要JRE就能运行你的jar包,无需安装JDK。
JDK在JRE的基础上进一步增加了开发工具。javac(编译器)、javap(反编译器)、javadoc(文档生成)、jconsole(监控工具)等都是JDK特有的,JRE中并不包含。JDK的定位是"开发Java程序所需的全套工具"。
JDK目录结构详解
以下是典型JDK安装目录的剖析(以JDK 17为例):
JDK_HOME/ ├── bin/ # 可执行文件,所有开发工具和运行时工具 │ ├── java* # JVM启动器 │ ├── javac* # Java编译器(JDK特有) │ ├── javap* # 字节码反编译器(JDK特有) │ ├── javadoc* # 文档生成器(JDK特有) │ ├── jar* # 打包工具 │ ├── jshell* # 交互式REPL(JDK 9+) │ ├── jlink* # 自定义运行时镜像(JDK 9+) │ ├── jpackage* # 平台安装包打包(JDK 14+) │ ├── jstat*/jmap*/jstack* # 诊断和监控工具 │ └── jdb* # 调试器 │ ├── conf/ # JDK配置(JDK 9+结构变更) │ ├── net.properties # 网络配置 │ └── security/ # 安全策略和证书 │ └── java.security │ ├── include/ # C/C++头文件,用于JNI开发 │ ├── jni.h │ ├── jvmti.h │ └── win32/jni_md.h # 平台相关的头文件 │ ├── jmods/ # JDK模块文件(JDK 9+新增) │ ├── java.base.jmod # 基础模块 │ ├── java.desktop.jmod # 桌面模块 │ ├── java.sql.jmod # SQL模块 │ └── ... │ ├── legal/ # 各模块的许可证信息(JDK 9+) │ ├── java.base/ │ ├── java.desktop/ │ └── ... │ └── lib/ # JDK内部使用的库 ├── src.zip # Java标准库源码(学习利器) ├── jrt-fs.jar # JRT文件系统提供者(JDK 9+) └── ...值得注意的结构变化:
JDK 8及之前:JRE作为一个完全独立的子目录存在(jdk/jre/),内部包含自己的bin/和lib/目录,其中lib/rt.jar包含了所有Java标准类库。如果你只安装了JRE而非JDK,你的安装目录就是JRE本身的目录结构。
JDK 9到JDK 10:JRE目录仍然存在于JDK中,但标准库已经从rt.jar迁移到了模块化格式(jmods/目录中的.jmod文件)。运行时JVM通过jrt://文件系统协议访问模块化类库。这种中间过渡状态存在的时间很短。
JDK 11及之后:JRE独立子目录正式消失。JDK目录本身就是运行时环境——运行Java程序所需的模块化类库通过jrt-fs.jar文件系统提供。如果需要部署时创建精简的JRE,使用jlink工具从JDK中生成一个自定义运行时镜像。这就是为什么Oracle官网从JDK 11起不再提供独立的JRE下载包。
rt.jar时代与Jigsaw模块化变革
rt.jar的辉煌与困境
在JDK 8及之前,Java标准类库被打包成一个巨大的rt.jar文件(Runtime JAR),大小约60MB,包含数万个类。这个文件是所有Java应用的基石——你在代码中使用的String、HashMap、ArrayList等都在这里。
rt.jar的问题是单体架构的顽疾:
- 任何Java应用都要加载整个rt.jar,即使你只是一个"Hello World"程序,所有类库的元数据都要被类加载器扫描一遍
- 内部API的滥用泛滥:
sun.misc.Unsafe、sun.misc.BASE64Encoder等本应为内部使用的类,因为被放在具有包访问控制的公开位置,被无数第三方库直接调用,使得JDK升级变得异常困难 - 部署体积庞大:要为嵌入式设备或Docker镜像打包JRE时,无法只选取需要的类库子集
Project Jigsaw与模块化系统
JDK 9引入的Java平台模块系统(JPMS,即Project Jigsaw)从根本上解决了上述问题。核心改变包括:
1. JDK自身被模块化:rt.jar不再存在,取而代之的是约90个标准模块(Module)。例如:
java.base → java.lang, java.util, java.io 等核心类 java.sql → JDBC相关类 java.xml → XML处理类 java.desktop → AWT, Swing等GUI类 java.logging → 日志框架java.base是唯一所有模块都隐式依赖的根模块。这种设计意味着一个不依赖GUI的应用在运行时完全不会加载java.desktop模块。
2. 强封装机制:模块通过module-info.java显式声明哪些包是公开API(exports),哪些包是内部实现。sun.misc.Unsafe等内部类默认不再对应用代码可见。这从编译器/运行时层面强制了API边界——不再只是文档上的"建议"。
3. 自定义运行时镜像:jlink工具可以根据应用的模块依赖,生成一个只包含所需模块的精简运行时镜像:
# 为你的应用创建一个最小化的运行时jlink --module-path$JAVA_HOME/jmods\--add-modules java.base,java.sql\--outputmy-minimal-runtime\--strip-debug--compress=2生成的精简运行时通常只有30-50MB,相比完整JRE的200MB+大幅减小。这在Docker镜像构建中尤为有价值——更小的镜像意味着更快的部署和更低的存储成本。
核心工具详解
java - JVM启动器
Java应用世界的"万能入口"。它的启动逻辑可以简化为:
- 加载JVM动态库(
jvm.dll/libjvm.so) - 创建JVM实例并设置启动参数
- 通过类加载器找到主类(
-jar指定jar包或直接指定类名) - 调用主类的
main(String[])方法
关键选项:
java-Xms512m-Xmx2g-XX:+UseG1GC-jarapp.jar# 设置堆大小和GCjava-cplib/*:myapp.jar com.example.Main# 手动指定classpathjava--add-opens java.base/java.lang=ALL-UNNAMED# JDK 9+: 开放封装-jarapp.jarjavac - Java编译器
将.java源文件编译为.class字节码文件。关键选项:
javac-dout/ src/com/example/*.java# 指定输出目录javac--release8Main.java# 兼容JDK 8(同时控制source/target和API)javac-parametersMain.java# 保留方法参数名(JDK 8+)javac-g:noneMain.java# 不生成调试信息javap - 字节码反编译器
学习JVM内部机制的利器。常用选项:
javap-cMain.class# 反编译方法字节码javap-verboseMain.class# 显示完整信息(常量池、属性等)javap-pMain.class# 包含私有成员javap-sysinfoMain.class# 显示类文件版本信息jlink - 运行时镜像构建(JDK 9+)
jlink --module-path jmods\--add-modules java.base,java.logging\--outputmyjrejpackage - 平台安装包生成(JDK 14+)
jpackage--inputtarget/--nameMyApp\--main-jar myapp.jar --main-class com.example.Main实践:如何选择JDK发行版
市场上JDK发行版的选择令人眼花缭乱,以下是基于实际使用经验的建议框架:
选择决策树
是否必须Oracle商业支持且预算允许? ├── 是 → Oracle JDK (LTS版本,付费订阅获得补丁) └── 否 → 使用OpenJDK衍生发行版 ├── 通用场景 → Adoptium (Eclipse Temurin) │ 特点:Eclipse基金会背书,TCK合规,覆盖面最广 │ ├── 国内大规模部署 → 阿里Dragonwell │ 特点:JWarmup预热、多租户GC、阿里大规模验证 │ ├── 云原生微服务 → 考虑OpenJ9 + Adoptium │ 特点:启动快、内存低、共享类缓存 │ ├── 多语言/GraalVM → GraalVM Community / Oracle GraalVM │ 特点:Native Image + Truffle多语言支持 │ └── 嵌入式/IoT → Liberica JDK (BellSoft) 特点:ARM/MIPS等小众架构支持广版本选择的核心决策
| 因素 | 建议 |
|---|---|
| 稳定生产 | JDK 17 LTS 或 JDK 21 LTS |
| 存量系统 | JDK 8(但需制定迁移计划) |
| 前沿特性 | 最新LTS版本 |
| 容器化部署 | JDK 17+(更好的容器感知) |
| Spring Boot 3 | JDK 17起(强依赖要求) |
几个常见误区澄清
误区一:“Oracle JDK才是官方JDK”。实际上所有TCK合规的OpenJDK发行版在执行Java程序的功能上是等效的。Oracle JDK与其他发行版共享同一套OpenJDK源码,差异主要在于许可证、打包格式和支持服务。
误区二:“JDK 8最稳定,新版本有未知问题”。JDK 8发布至今超过10年,而JDK 17和21已经过了大规模生产验证。新版本在GC性能、容器感知、安全补丁方面有显著优势。固守JDK 8的唯一合理理由是应用依赖的框架/中间件不兼容更高版本,而这本身就是一个需要解决的工程债务。
误区三:“安装JDK就是安装了JVM”。严格来说是安装了包含JVM的完整开发环境。如果你在服务器上只部署应用,安装的是JRE或者通过jlink生成的精简运行时,而非完整的JDK。
实践要点
生产服务器不应安装完整JDK:安全意识上,生产服务器应遵循"最小权限"原则。JDK中包含
jmap、jstack等工具,在攻击者获得shell访问后可能被利用。理想的部署方案是通过jlink为应用定制精简运行时,既减小体积,又减少攻击面。Docker镜像中的JDK选择:推荐使用
eclipse-temurin:17-jre(或对应的-jre-alpine)作为基础镜像。如果需要诊断工具,使用-jdk版本但在生产环境通过配置禁用远程调试和JMX。多阶段构建中可以仅在构建阶段使用JDK完整镜像,运行阶段切换为JRE精简镜像。模块化迁移不是必须的:JDK 9的模块化系统对应用代码是可选的——你的应用即使没有
module-info.java,仍然可以正常编译和运行。只有当你的库面向广泛的开发者公开分发时,模块化封装才具有较高的价值。对于内部应用,模块化迁移的投入产出比需要仔细评估。rt.jar消失的影响:如果你维护的代码中直接依赖了
rt.jar的路径(如某些老旧的构建脚本或类加载器实现),在JDK 9+上需要适配。解决方案是使用-Xbootclasspath/a(不推荐,逐版本变化大)或直接使用标准API。javap是学习JVM的窗口:在技术成长过程中,
javap -c -verbose是理解编译器行为、字节码指令、常量池结构的最有效工具。建议在遇到语法糖(如Lambda、字符串switch、try-with-resources)时,用javap查看编译器究竟生成了什么字节码,这比读100篇博客都管用。
小结
- JDK ⊃ JRE ⊃ JVM 是三者包含关系的经典表述,JDK提供开发工具集,JRE提供运行时库,JVM是字节码执行的核心引擎
- JDK 9的Jigsaw模块化系统的本质是将单体rt.jar拆分为可控的模块,带来了更强的封装和更精简的运行时
jlink工具允许创建只包含所需模块的自定义运行时,大幅减少部署体积,在容器化和微服务场景下价值突出- 选择JDK发行版时,优先考虑TCK合规的OpenJDK发行版(Adoptium是稳妥的首选),LTS版本是最务实的选择
- JDK/JRE/JVM的关系辨析看似基础,其实构成了所有JVM知识的地基——理解"谁提供什么"是后续深入调优和排障的前提
