「Java 进阶之路」系列 Day15
写在前面
模块一并发编程系列写完了,从这篇开始进入模块二,回过头补一补 Java 最基础也最容易说不清楚的几个概念。第一个就是 JDK、JRE、JVM------这三个词几乎每个 Java 开发者天天挂在嘴边,但真要问"它们仨到底是什么关系",很多人只能含糊地说"JDK 比 JRE 大,JRE 比 JVM 大",再往下就说不清楚了。这篇把三者的边界、以及"为什么现在装了 JDK 却找不到独立的 JRE"这个容易让人困惑的现状一次讲透。
一、是什么:一层包含一层的关系
三者是严格的包含关系,从外到内依次是:
一句话理解各自的定位:JVM 负责"跑" (执行字节码);JRE 负责"跑得起来" (JVM 加上程序运行必需的类库,让编译好的程序能实际运行);JDK 负责"写、编译、跑"全套(JRE 之外再加上开发时需要的编译器、调试器这些工具)。
二、为什么要分这三层:不同的人只需要不同的东西
这套分层设计对应的是三种不同的使用者角色:
- 只想运行 Java 程序的普通用户:只需要 JRE,能把字节码跑起来就够了,不需要关心怎么把源码编译成字节码
- 要开发 Java 程序的程序员 :需要完整的 JDK,因为要用
javac把.java源码编译成.class字节码,还要用调试器排查问题 - 不管是谁,最终执行字节码的都是 JVM :这一层是三者里唯一真正需要为每个操作系统单独实现的部分,正是因为 JVM 把"跟操作系统打交道"这件事封装了起来,才实现了 Java 那句经典口号------"一次编译,到处运行"(Write Once, Run Anywhere):同一份
.class字节码,扔到 Windows、Linux、macOS 上的 JVM 里都能跑,不需要针对每个平台重新编译源码。
这也是为什么三者要设计成一层包一层,而不是各自独立:JDK 开发出来的程序,最终一定是通过 JRE 提供的 JVM 去运行的,所以 JDK 里天然要包含一份完整的 JRE,才能保证"编译完立刻能在本机跑起来验证"。
三、怎么用:从源码到运行的完整链路
用一段最简单的代码走一遍完整流程:
java
// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, JVM");
}
}
bash
# 用JDK提供的javac编译器,把.java源码编译成.class字节码
javac Hello.java
# 生成 Hello.class,这是一份跨平台的字节码文件,不是某个操作系统专属的可执行文件
# 用java命令启动JVM,加载并执行这份字节码
java Hello
# 输出:Hello, JVM
javac 是 JDK 独有的工具,JRE 里没有;而 java 这个启动 JVM 的命令,JRE 和 JDK 里都有------这也印证了前面"JRE 包含 JVM,JDK 包含 JRE"的层层关系。
一个容易让人困惑的现状:JRE 去哪了
如果你现在下载安装最新版 JDK,会发现安装目录里根本找不到一个独立叫 jre 的文件夹,只有 bin、lib 这些目录混在一起------这不是安装出了问题,而是从 JDK 9 引入模块化系统(Project Jigsaw)开始,官方就不再把 JDK 和 JRE 拆成两个独立的下载包分发了,JDK 11 之后更是彻底不单独提供 JRE 安装包。现在下载到的就是一个完整的 JDK,JRE 的能力已经内含在其中,不再是一个能单独感知到的物理文件夹。
这个变化背后还有一个更实用的产物:JDK 9 之后可以用 jlink 工具,根据你的程序实际用到的模块,定制打包出一个体积小得多的自定义运行时镜像,而不是笼统地打包一份包含所有类库的完整 JRE------这也是模块化之后官方索性不再单独维护"标准 JRE"这个产物的原因之一。
常见的坑:环境变量配置搞混
bash
# 常见的环境变量配置(以类Unix系统为例)
export JAVA_HOME=/usr/lib/jvm/jdk-21
export PATH=$JAVA_HOME/bin:$PATH
JAVA_HOME 约定俗成指向 JDK 的安装目录(不是 JRE),很多工具链(比如 Maven、Gradle)都是靠这个环境变量去找 javac 来编译代码的。如果历史遗留代码或者旧文档让你把 JAVA_HOME 指向了某个单独的 JRE 目录,会导致编译阶段直接报错找不到 javac------这也是"JDK 包含 JRE,反过来不成立"这条包含关系在实际踩坑时最容易体现出来的地方。
四、面试追问
Q1:JDK、JRE、JVM 三者是什么关系?
是严格的包含关系:JDK 包含 JRE,JRE 包含 JVM。JVM 负责执行字节码;JRE 是 JVM 加上程序运行必需的核心类库,让编译好的程序能实际跑起来;JDK 是 JRE 之外再加上 javac、jar、调试器这些开发工具,面向的是要编写和编译代码的开发者。
Q2:为什么说 JVM 是 Java 实现"一次编译到处运行"的关键?
因为 Java 源码编译出来的是统一格式的字节码(.class 文件),这份字节码本身不跟任何操作系统绑定;真正需要针对不同操作系统单独实现的,是负责加载和执行这份字节码的 JVM。不同平台各自实现自己的 JVM,但只要都遵循同一套字节码规范,同一份 .class 文件就能在任意装了对应 JVM 的平台上运行,不需要重新编译源码。
Q3:只想运行别人写好的 Java 程序,需要安装 JDK 吗?
理论上只需要 JRE 就够了,因为运行程序不需要 javac 这类开发工具,只需要 JVM 加上核心类库。不过实际情况是,JDK 9 之后官方已经不单独发布 JRE 安装包了,现在市面上能下载到的基本都是完整的 JDK,所以即使只是运行程序,通常也是装的 JDK,只是用不到里面的开发工具部分而已。
Q4:为什么 JDK 9 之后官方不再单独发布 JRE?
JDK 9 引入了模块化系统(Project Jigsaw),Java 类库被拆分成了一个个独立的模块。在这个基础上,官方提供了 jlink 工具,可以根据程序实际依赖的模块,按需定制打包出一个体积更小的自定义运行时镜像,而不再需要一份笼统打包了所有类库的"标准 JRE"。这让单独维护和发布一份通用 JRE 的意义大大降低,所以从 JDK 11 起就彻底不再提供独立的 JRE 下载包了。
Q5:JAVA_HOME 这个环境变量应该指向 JDK 目录还是 JRE 目录?
应该指向 JDK 的安装目录。这是因为编译工具链(比如 Maven、Gradle)以及很多依赖 Java 环境的工具,都是通过 JAVA_HOME/bin 去查找 javac 编译器的,而 javac 只存在于 JDK 里、不存在于单独的 JRE 里。如果误把 JAVA_HOME 指向了一个只有 JRE 的目录,编译阶段会直接报错找不到编译器。
下一篇预告
Day16 讲基本数据类型和类型转换里容易踩的坑------精度丢失是怎么发生的、自动拆装箱又藏着哪些看不见的性能陷阱和空指针风险。