JDK、JRE、JVM 详解
本章定位:深入理解 Java 技术体系中最核心的三个概念——JDK(开发工具包)、JRE(运行环境)、JVM(虚拟机)。它们是 Java "一次编写,到处运行"的技术基石,也是面试中几乎必考的送分题。大翔在技术选型会上用这三层关系说服了 CTO,白歌在代码审查时用它定位了环境问题,小崔在面试中被问到时对答如流——本章帮你达到小崔的水平。
定义速览表
| 组件 | 英文全称 | 中文译名 | 一句话定义 | 包含内容 | 目标用户 |
|---|---|---|---|---|---|
| JDK | Java Development Kit | Java 开发工具包 | 开发和运行 Java 程序的完整环境 | JRE + 开发工具(javac、jar、javadoc 等) | Java 开发者 |
| JRE | Java Runtime Environment | Java 运行时环境 | 仅运行 Java 程序的最小环境 | JVM + 核心类库(rt.jar) | 最终用户 |
| JVM | Java Virtual Machine | Java 虚拟机 | 执行 Java 字节码的抽象计算机 | 类加载器 + 运行时数据区 + 执行引擎 | 任何运行 Java 的地方 |
包含关系
JDK 包含 JRE,JRE 包含 JVM。这是一个层层嵌套的结构:
关键结论:
- JDK 面向开发者,是一个完整的开发工具包,除了能运行 Java 程序,还能编译、打包、调试
- JRE 面向最终用户,只提供运行 Java 程序所需的最小环境
- JVM 是 Java 语言跨平台的核心,所有 Java 程序最终都运行在 JVM 上
JDK 详解:开发者的瑞士军刀
JDK 目录结构
安装 JDK 8 后,典型的目录布局如下:
C:\Program Files\Java\jdk1.8.0_202\
├── bin/ ← 可执行文件目录
│ ├── javac.exe ← Java 编译器(源码 → 字节码)
│ ├── java.exe ← Java 启动器(启动 JVM 运行字节码)
│ ├── jar.exe ← 打包工具(.class → .jar)
│ ├── javadoc.exe ← 文档生成工具
│ ├── javap.exe ← 字节码反编译查看器
│ ├── jdb.exe ← 命令行调试器
│ └── ...
├── jre/ ← JDK 自带的私有 JRE
│ ├── bin/
│ │ └── java.exe ← JRE 的 Java 启动器
│ └── lib/
│ ├── rt.jar ← 核心类库(Runtime,约 60MB)
│ ├── charsets.jar
│ └── ...
├── lib/ ← JDK 开发工具库
│ ├── tools.jar ← javac 等编译器核心实现
│ └── dt.jar ← BeanInfo 等设计时库
├── include/ ← JNI 本地方法调用 C 头文件
└── src.zip ← JDK 核心类库源码(学习利器)
私有 JRE vs 公共 JRE
JDK 8 安装时会安装两份 JRE:
| 类型 | 路径 | 用途 |
|---|---|---|
| 私有 JRE | jdk1.8.0_202/jre/ | 仅供 JDK 自身工具使用(javac 等内部需要 JRE 运行) |
| 公共 JRE | C:\Program Files\Java\jre1.8.0_202/ | 供系统上所有 Java 程序使用,注册到系统 Path |
公共 JRE 在安装时可取消勾选。如果只做开发,私有 JRE 已经足够。之所以存在两份,是因为 JDK 的历史设计——javac 本身是用 Java 写的,编译过程需要一个 JRE 来运行编译器。
JRE 详解:运行时的最小环境
JRE 的核心组成
JRE = JVM + 核心类库。其中核心类库以 rt.jar(Runtime JAR)的形式存在,包含了所有 Java 标准 API:
| 包 | 功能 | 典型类 |
|---|---|---|
java.lang | 语言基础类 | String、Object、System、Thread |
java.util | 集合框架与工具 | ArrayList、HashMap、Date、Collections |
java.io | 输入输出 | File、InputStream、BufferedReader |
java.net | 网络编程 | Socket、URL、HttpURLConnection |
java.sql | 数据库访问 | Connection、ResultSet、DriverManager |
java.math | 高精度数学运算 | BigDecimal、BigInteger |
rt.jar 在 JDK 8 中约 60MB,包含约 4000+ 个类。没有它,即使 JVM 能加载字节码,也无法执行任何有意义的 Java 程序(连 System.out.println 都需要 rt.jar 中的 System 类)。
JVM 详解:跨平台的核心引擎
JVM 内部架构
运行时数据区详解
| 区域 | 线程共享 | 存储内容 | 异常 |
|---|---|---|---|
| 堆(Heap) | 是(所有线程共享) | 对象实例、数组 | OutOfMemoryError |
| 方法区(Method Area) | 是 | 类信息、常量、静态变量、JIT 编译后的代码 | OutOfMemoryError |
| Java 栈(Stack) | 否(每个线程独立) | 局部变量表、操作数栈、动态链接、返回地址 | StackOverflowError |
| 程序计数器(PC) | 否(每个线程独立) | 当前线程所执行字节码的行号指示器 | 唯一不抛 OOM 的区域 |
| 本地方法栈 | 否(每个线程独立) | 本地方法(Native Method)的调用信息 | StackOverflowError |
JIT 编译器:热点代码优化
JDK 8 默认使用分层编译(Tiered Compilation):解释执行 → C1 编译(快速优化) → C2 编译(深度优化)。方法被调用达到一定次数(默认约 10000 次)后触发 JIT 编译。
三者协作流程:从源码到运行
完整流程:
- 开发者使用 JDK(javac.exe)将
.java源码编译为.class字节码 - 用户或开发者使用 JRE(java.exe)启动 JVM
- JVM 的类加载器读取字节码,经过校验后,结合 rt.jar 核心类库,由执行引擎解释或编译执行
完整代码示例
示例一:验证 JDK、JRE、JVM 版本与环境信息
/**
* 场景:白歌让小崔确认三台开发机的 JDK/JRE/JVM 版本是否一致。
* 小崔写了这个工具类,一键输出所有版本信息。
*/
public class JavaVersionChecker {
public static void main(String[] args) {
System.out.println("═══════════════════════════════════════");
System.out.println(" JDK / JRE / JVM 环境信息检测");
System.out.println("═══════════════════════════════════════\n");
// 1. JDK 版本(开发环境版本)
String javaVersion = System.getProperty("java.version");
System.out.println("JDK 版本: " + javaVersion);
// 2. JRE 版本(运行环境版本)
String javaRuntime = System.getProperty("java.runtime.version");
System.out.println("JRE 版本: " + javaRuntime);
// 3. JVM 信息
String jvmName = System.getProperty("java.vm.name");
String jvmVersion = System.getProperty("java.vm.version");
String jvmVendor = System.getProperty("java.vm.vendor");
System.out.println("JVM 名称: " + jvmName);
System.out.println("JVM 版本: " + jvmVersion);
System.out.println("JVM 供应商: " + jvmVendor);
System.out.println("JVM 规范版本: " + System.getProperty("java.vm.specification.version"));
// 4. 类路径与安装路径
System.out.println("\nJDK 安装路径: " + System.getProperty("java.home"));
System.out.println("类路径: " + System.getProperty("java.class.path"));
// 5. JVM 内存信息(堆内存默认大小)
long maxMemory = Runtime.getRuntime().maxMemory() / (1024 * 1024);
long totalMemory = Runtime.getRuntime().totalMemory() / (1024 * 1024);
long freeMemory = Runtime.getRuntime().freeMemory() / (1024 * 1024);
System.out.println("\nJVM 堆内存信息:");
System.out.println(" 最大可用: " + maxMemory + " MB");
System.out.println(" 已分配: " + totalMemory + " MB");
System.out.println(" 空闲: " + freeMemory + " MB");
// 6. 判断是否 HotSpot JVM
System.out.println("\nJVM 类型判断:");
if (jvmName.contains("HotSpot")) {
System.out.println(" ✓ 当前使用的是 Oracle HotSpot JVM(含 C2 JIT 编译器)");
} else {
System.out.println(" → 当前使用的是其他 JVM 实现");
}
// 7. 可用处理器数量(影响 GC 并行度)
System.out.println("\n系统资源:");
System.out.println(" 可用处理器: " + Runtime.getRuntime().availableProcessors() + " 核");
}
}
═══════════════════════════════════════
JDK / JRE / JVM 环境信息检测
═══════════════════════════════════════
JDK 版本: 1.8.0_202
JRE 版本: 1.8.0_202-b08
JVM 名称: Java HotSpot(TM) 64-Bit Server VM
JVM 版本: 25.202-b08
JVM 供应商: Oracle Corporation
JVM 规范版本: 1.8
JDK 安装路径: C:\Program Files\Java\jdk1.8.0_202\jre
类路径: .
JVM 堆内存信息:
最大可用: 1820 MB
已分配: 123 MB
空闲: 120 MB
JVM 类型判断:
✓ 当前使用的是 Oracle HotSpot JVM(含 C2 JIT 编译器)
系统资源:
可用处理器: 8 核
示例二:从 .java 到 .class —— 手动走完编译运行全链路
/**
* 场景:小崔想知道 javac 到底做了什么,白歌让他手动编译并反编译,
* 把 JDK → JRE → JVM 的全链路走一遍。
*
* 操作步骤(在命令行中执行):
* 步骤1(JDK 层):javac CompileDemo.java → 生成 CompileDemo.class
* 步骤2(JDK 层):javap -c CompileDemo.class → 查看字节码指令
* 步骤3(JDK 层):javap -verbose CompileDemo.class → 查看常量池等详细信息
* 步骤4(JRE 层):java CompileDemo → JVM 加载并执行
* 步骤5(JVM 层):java -XX:+PrintCompilation CompileDemo → 查看 JIT 编译过程
*/
public class CompileDemo {
public static void main(String[] args) {
String message = "Hello, JVM!";
int count = 3;
// 这段循环在解释执行阶段会被解释器逐条执行
// 随着调用次数增加,可能会触发 JIT 编译优化
for (int i = 0; i < count; i++) {
System.out.println(message + " - 第" + (i + 1) + "次");
}
// 打印执行环境信息,验证运行在 JVM 之上
System.out.println("\n本程序运行在: "
+ System.getProperty("java.vm.name")
+ " v" + System.getProperty("java.vm.version"));
}
}
命令行操作及预期输出:
步骤1 — 编译(JDK 的 javac):
> javac CompileDemo.java
(无输出表示编译成功,生成 CompileDemo.class)
步骤2 — 反编译查看字节码(JDK 的 javap):
> javap -c CompileDemo.class
Compiled from "CompileDemo.java"
public class CompileDemo {
public CompileDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String Hello, JVM!
2: astore_1
3: iconst_3
4: istore_2
5: iconst_0
6: istore_3
7: iload_3
8: iload_2
9: if_icmpge 50
12: getstatic #3 // Field java/lang/System.out:...
...
}
步骤3 — 查看 JVM 常量池与版本信息:
> javap -verbose CompileDemo.class
(输出包含:魔数 CA FE BA BE、主次版本号 52.0 = JDK 8、
常量池 #1~#30、方法字节码、行号表等)
步骤4 — 运行(JRE 的 java 命令启动 JVM):
> java CompileDemo
Hello, JVM! - 第1次
Hello, JVM! - 第2次
Hello, JVM! - 第3次
本程序运行在: Java HotSpot(TM) 64-Bit Server VM v25.202-b08
步骤5 — 观察 JIT 编译(JVM 内部):
> java -XX:+PrintCompilation CompileDemo
(输出 JIT 编译的方法列表,通常 main 方法不会被编译,
但 System.out.println 的内部方法可能会被编译)
易错场景
反例一:把 JDK 目录下的 JRE 当成"唯一的 JRE"
# ❌ 错误:删除 JDK 目录后,认为系统上还有公共 JRE 可以运行程序
# 实际情况:如果安装 JDK 时取消了公共 JRE,删除 JDK 目录后系统将无法运行 Java 程序
纠正:安装 JDK 8 时会安装两份 JRE。私有 JRE 在 jdk1.8.0_202/jre/,公共 JRE 在 C:\Program Files\Java\jre1.8.0_202/。两者的 java.exe 互相独立。如果你的 Java 程序依赖公共 JRE 的 Path 注册,删除 JDK 不影响它;但如果你的 Path 只指向了 JDK 的 bin 目录,删除 JDK 后 java 命令就不可用了。
反例二:误以为 java 命令直接执行 .class 文件
# ❌ 错误:这样写会报错
> java Hello.class
错误: 找不到或无法加载主类 Hello.class
# ✅ 正确:java 命令的参数是类名,不是文件名
> java Hello
原理:java 命令启动 JVM 后,JVM 的类加载器根据完全限定类名去 classpath 中查找字节码文件。你传入的是类的名字(如 Hello),而不是文件名(Hello.class)。JVM 会自动加上 .class 后缀去搜索。
反例三:误以为 JRE = 不需要 JDK 就能开发
# ❌ 错误:只安装了 JRE,却想编译 Java 源码
> javac Hello.java
'javac' 不是内部或外部命令,也不是可运行的程序
# JRE 只包含 java 命令(运行),不包含 javac 命令(编译)
原理:javac 编译器本身就在 JDK 的 lib/tools.jar 中(JDK 9+ 改为模块),JRE 为了保持体积精简,不包含任何开发工具。开发必须 JDK,运行只需 JRE。
面试考点
Q1:JDK、JRE、JVM 分别是什么?它们之间的关系?
JDK(Java Development Kit)是 Java 开发工具包,面向开发者,包含 JRE + 开发工具(javac、jar、javadoc 等)。JRE(Java Runtime Environment)是 Java 运行时环境,面向最终用户,包含 JVM + 核心类库(rt.jar)。JVM(Java Virtual Machine)是 Java 虚拟机,是执行字节码的抽象计算机,包含类加载器、运行时数据区、执行引擎。
三者关系:JDK ⊃ JRE ⊃ JVM,是层层包含的关系。JDK 包含 JRE,JRE 包含 JVM。如果你需要开发 Java 程序,必须安装 JDK;如果只需要运行 Java 程序,JRE 就够了;任何 Java 程序最终都运行在 JVM 上。
Q2:为什么 Java 能"一次编写,到处运行"?
Java 的"一次编写,到处运行"并非在操作系统层面直接实现,而是通过引入字节码 + JVM 两层抽象:
- Java 源码(
.java)被 JDK 的 javac 编译器编译为平台无关的字节码(.class)。字节码是对 JVM 的指令集,与 CPU 架构无关。- 不同操作系统安装各自的 JVM 实现(Windows JVM、Linux JVM、macOS JVM),JVM 负责将同一份字节码翻译为本地的机器指令。
- 开发者只需编译一次,生成的
.class文件可以在任何安装了 JVM 的设备上运行。关键:JVM 充当了字节码与操作系统之间的翻译层。这也是为什么 Java 被称为"半编译半解释"语言——编译到字节码是编译,JVM 执行字节码是解释(或 JIT 编译)。
Q3:JVM 的运行时数据区包含哪些部分?各自用途?
区域 线程共享性 存储内容 可能异常 堆(Heap) 所有线程共享 对象实例、数组 OutOfMemoryError 方法区(Method Area) 所有线程共享 类信息、常量池、静态变量 OutOfMemoryError Java 栈(Stack) 线程私有 局部变量表、操作数栈 StackOverflowError 程序计数器(PC) 线程私有 当前字节码行号 无 OOM 本地方法栈 线程私有 Native 方法调用信息 StackOverflowError 堆是 GC 的主要工作区域,也是 JVM 调优的核心。方法区在 HotSpot 中的实现是"永久代"(JDK 7 及以前)或"元空间"(JDK 8+)。Java 栈和程序计数器随线程创建而创建、销毁而释放。
Q4:JDK 中的私有 JRE 和公共 JRE 有什么区别?
JDK 8 安装时会安装两份 JRE:
- 私有 JRE(
jdk目录/jre/):仅供 JDK 自身开发工具使用。因为 javac 等工具本身是用 Java 写的,编译过程需要一个 JRE 来运行编译器。- 公共 JRE(
C:\Program Files\Java\jre版本号/):注册到操作系统,供系统上所有 Java 程序使用。安装时可取消勾选。如果只做开发,私有 JRE 已经足够。从 JDK 11 开始,Oracle 不再提供单独的 JRE 安装包,JDK 自带运行环境——JDK 和 JRE 的概念边界在逐渐模糊。
Q5:javac 和 java 命令分别做了什么?它们属于 JDK 还是 JRE?
javac(Java Compiler)属于 JDK,负责将.java源码编译为.class字节码。它读取源文件中的类、方法、语句,经过词法分析 → 语法分析 → 语义分析 → 字节码生成四个阶段,输出平台无关的.class文件。
java(Java Launcher)同时存在于 JDK 和 JRE 中,负责启动 JVM 并执行字节码。它首先创建 JVM 实例,通过类加载器加载指定的主类,校验字节码,然后从main()方法开始执行。简单记忆:javac 是翻译官,把人类能看懂的 Java 代码翻译成 JVM 能看懂的字节码;java 是驾驶员,发动 JVM 这台发动机去跑字节码。