JDK、JRE 与 JVM 的关系
本教程共 100 篇 · 第 3 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
3. JDK、JRE 与 JVM 的关系
本节目标:分清 JDK、JRE、JVM 三个缩写各自负责什么,理解字节码怎么被执行,装环境时知道该装哪个。
三个缩写,一层套一层
初学者最容易被这三个词绕晕。先记全称:
- JVM = Java Virtual Machine,Java 虚拟机
- JRE = Java Runtime Environment,Java 运行时环境
- JDK = Java Development Kit,Java 开发工具包
它们不是三个并列的东西,而是一层套一层的包含关系:
┌─ ┌────────────────────────────────────┐
│ │ javac、jar、javadoc、jshell ... │ ← 开发工具
│ └────────────────────────────────────┘
JDK ┌─┌────────────────────────────────────┐
│ │ │ │
│ JRE│ JVM + 标准类库 │ ← 运行所需
│ │ │ │
└─ └─└────────────────────────────────────┘
┌───────┐┌───────┐┌───────┐┌───────┐
│Windows││ Linux ││ macOS ││others │ ← 操作系统
└───────┘└───────┘└───────┘└───────┘
```bash
一句话版本:**JDK 包含 JRE,JRE 包含 JVM**。
要写 Java,装 JDK 就够了,它把另外两个都带上了。
## JVM:真正干活的那一层
JVM 是这套体系的核心。它是一个**软件模拟出来的「计算机」**,有自己的指令集、自己的内存布局。
它干四件事:
1. **加载**:把 `.class` 文件读进内存。
2. **校验**:检查字节码是否合法,防止恶意代码搞破坏。
3. **执行**:把字节码指令翻译成当前 CPU 能懂的机器码并运行。
4. **管内存**:分配对象、回收垃圾(GC),不用你手动释放。
如 §01 所述,JVM 像翻译官——字节码是「世界语」,JVM 负责讲成当地话。
关键在于:**Windows、Linux、macOS 各有各的 JVM,但它们读的是同一份字节码**。跨平台就是这么实现的。
> [!NOTE]
> 严格说 JVM 是一份「规范」,不是一个具体软件。Oracle HotSpot、OpenJ9、GraalVM 都是它的实现。日常说 JVM 一般指 HotSpot。
## 字节码长什么样
光说不练没感觉。写个最短的程序看看:
```java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}
编译它,然后用 JDK 自带的 javap 反汇编:
javac Hello.java
javap -c Hello.class
```java
你会看到类似这样的输出:
```text
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello
5: invokevirtual #15 // Method java/io/PrintStream.println:(...)V
8: return
}
getstatic、ldc、invokevirtual 就是字节码指令,长得像汇编但不属于任何真实 CPU。
它们只有 JVM 认识。这就是「字节码是 JVM 的机器语言」这句话的含义。
Tip
#7、#13这些编号指向常量池,不同 JDK 版本编出来的数字会不一样,不用对着抄。看懂指令名就够了。
解释执行 + JIT:Java 为什么不慢
早期 Java 被吐槽慢,因为 JVM 一条条解释字节码,比机器码慢一大截。
现在的 JVM 加了 JIT(Just-In-Time)编译器。它会盯着程序运行,发现某段代码被反复执行(比如循环里的方法),就把它直接编译成本地机器码缓存起来。
下次再跑到这段代码,走的是机器码,速度接近 C。
所以现代 Java 的性能表现是:启动稍慢,跑起来很快。长期运行的服务端程序正好吃这个红利。
JRE:只想跑,不想写
JRE = JVM + 标准类库。
标准类库就是 String、ArrayList、Math 这些你即将天天用到的现成类。它们已经编译成字节码,打包在 JDK 里。
只装 JRE 的机器可以运行 Java 程序,但没有编译器,写不了代码。
Warning从 Java 11 开始,Oracle 不再单独发布 JRE 安装包。现在统一装 JDK;如果只想给用户一个精简运行时,用
jlink工具自己裁剪一个。老教程里「先装 JRE 再装 JDK」的说法已经过时了。
JDK:开发者要的那一套
JDK 在 JRE 之上补齐了开发工具。装完 JDK,在它的 bin 目录下能找到一堆可执行文件:
| 命令 | 作用 |
|---|---|
java | 启动 JVM,运行程序 |
javac | 编译器,.java → .class |
jar | 把一堆 .class 打包成 .jar |
javadoc | 从文档注释生成 HTML API 文档 |
javap | 反汇编,查看字节码 |
jshell | 交互式命令行(Java 9 引入),随手试语法 |
jdb | 命令行调试器 |
jlink | 裁剪定制运行时镜像 |
初学阶段用得最多的是 javac 和 java。jshell 也值得记住,试一小段语法不用建文件。
class 文件版本号:高版本编的跑不了低版本
每个 .class 文件里都记着一个版本号,标明它是哪个 JDK 编出来的。
| Java 版本 | class 文件版本 |
|---|---|
| Java 8 | 52 |
| Java 11 | 55 |
| Java 17 | 61 |
| Java 21 | 65 |
| Java 25 | 69 |
规则很简单:高版本 JVM 能跑低版本字节码,反过来不行。
用 Java 25 LTS 编译的类,扔到 Java 17 的环境里运行,会直接抛:
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more
recent version of the Java Runtime (class file version 69.0), this version
of the Java Runtime only recognizes class file versions up to 61.0
```bash
这个错误初学者踩得非常多,通常是本机 JDK 和服务器 JDK 版本不一致造成的。看到 `UnsupportedClassVersionError`,第一反应就该是查版本。
## 一个常被忽略的事实:JVM 不只跑 Java
JVM 认的是字节码,它并不关心这份字节码是从哪种语言编译来的。
于是出现了一批「JVM 语言」:
| 语言 | 特点 | 常见场景 |
|------|------|---------|
| Kotlin | 语法简洁,空安全 | Android、后端 |
| Scala | 函数式特性强 | 大数据(Spark) |
| Groovy | 动态类型,脚本友好 | Gradle 构建脚本 |
| Clojure | Lisp 方言 | 特定领域 |
它们编译出的 `.class` 文件和 Java 编译出的没有本质区别,能互相调用。
这也解释了为什么第 2 章说「学 Java 转 Kotlin 成本很低」——底层跑的是同一套东西。
反过来看,把 JVM 吃透的收益不止于一门语言。类加载、内存模型、GC 这些知识,换到 Kotlin、Scala 上照样成立。
## 完整流程串一遍
把上面的内容连起来,一个 Java 程序的一生是这样:
```text
你写代码 Hello.java (纯文本)
│ javac 编译
▼
class 文件 Hello.class (版本号 69)
│ java 启动 JVM
▼
JVM 加载 → 校验 → 解释执行 / JIT 编译 → 输出结果
javac 属于 JDK,java 启动的是 JVM,运行时用到的 String 等类来自 JRE 的标准类库。
三者各司其职,缺一环程序就跑不起来。
小结
JDK 套着 JRE,JRE 套着 JVM,开发只需要装 JDK。
JVM 负责加载、校验、执行字节码和管理内存,是跨平台的实现者;JIT 让长期运行的程序性能接近本地代码。
JRE 是 JVM 加标准类库,Java 11 起不再单独发布。
class 文件带版本号,高版本 JVM 兼容低版本字节码,反向会报 UnsupportedClassVersionError。