Appearance
JVM 架构详解
JVM 是什么、为什么需要?
定义: JVM(Java Virtual Machine)是JAVA虚拟计算机。
在没有 JVM 的时代,C/C++ 程序需要针对不同操作系统和 CPU 架构分别编译,同一份源码在 Windows 上编译出的 exe 无法在 Linux 上运行,在 x86 上编译的二进制无法在 ARM 上运行。Sun 公司的 James Gosling 团队在设计 Java 时提出了一个核心理念:(一次编写,到处运行)。因此JVM 就是为了解决这个跨平台问题而诞生的。它在操作系统和 Java 程序之间引入了一个抽象层。
如下图示:

Java程序运行的时候,编译器(Javac.exe)将.java文件编译成平台无关的字节码文件(.class), JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
JVM负责运行.class,只要能翻译成符合规范的.class文件,都能被JVM运行。
JVM是一个跨平台的运行环境,有自己的生态系统。
从宏观角度,JVM它定义了指令集、寄存器集、类文件结构、堆栈、垃圾回收堆和内存区域等规范。JVM 规范本身不规定实现细节,因此有了不同的 JVM 实现。
主流 JVM 实现
| 实现 | 开发商 | 特点 |
|---|---|---|
| HotSpot VM | Oracle / OpenJDK | 最主流,混合模式解释+编译 |
| OpenJ9 | IBM / Eclipse | 低内存占用,快速启动 |
| GraalVM | Oracle | 多语言运行时,AOT 编译 |
| Zing VM | Azul | C4 垃圾回收器,超低延迟 |
JVM 的核心价值
| 价值 | 说明 |
|---|---|
| 跨平台 | 字节码一次编译,任意 JVM 运行 |
| 内存管理 | 自动垃圾回收,无需手动 free/delete |
| 安全性 | 沙箱机制,字节码校验,防止恶意代码 |
| 性能优化 | 运行时 JIT 编译,热点代码优化 |
| 生态基础 | Kotlin、Scala、Groovy 等语言均运行在 JVM 上 |
JVM 整体架构
JVM 架构可以分为三大子系统 + 两大核心组件:
+---------------------------------------------------------------+
| JVM 架构总览 |
| |
| +------------------+ +------------------+ +------------------+
| | 类加载器子系统 | | 运行时数据区 | | 执行引擎 |
| | ClassLoader | | Runtime Data | | Execution Engine|
| | Subsystem | | Areas | | |
| | | | | | |
| | Bootstrap | | +-----------+ | | 解释器 (Interpreter)|
| | +-----------+ | | | 方法区 | | | JIT 编译器 (C1/C2) |
| | | Extension | | | | (Method | | | 垃圾回收器 (GC) |
| | +-----------+ | | | Area) | | | |
| | | Application| | | +-----------+ | +------------------+
| | +-----------+ | | | 堆 (Heap) | |
| | | Custom | | | +-----------+ |
| | +-----------+ | | | 栈 (Stack) | |
| +------------------+ | +-----------+ |
| | | 程序计数器 | |
| | | (PC Register)| |
| | +-----------+ |
| | | 本地方法栈 | |
| | | (Native | |
| | | Method | |
| | | Stack) | |
| | +-----------+ |
| +------------------+
+---------------------------------------------------------------+
| |
v v
+------------------+ +------------------+
| 本地方法接口 | | 本地方法库 |
| JNI (Java |----------------------->| Native Method |
| Native | | Libraries |
| Interface) | | |
+------------------+ +------------------+类加载器子系统(ClassLoader Subsystem)
负责加载 .class 文件到内存。类加载器分为三层:
- Bootstrap ClassLoader:加载
<JAVA_HOME>/lib下的核心类库(rt.jar, java.* 等),由 C++ 实现 - Extension ClassLoader(JDK 9+ 改为 Platform ClassLoader):加载
<JAVA_HOME>/lib/ext下的扩展类库 - Application ClassLoader:加载 classpath 下的应用类
运行时数据区(Runtime Data Areas)
JVM 在运行 Java 程序时会使用多种内存区域:
| 区域 | 线程共享 | 存放内容 | 生命周期 |
|---|---|---|---|
| 堆(Heap) | 是 | 所有对象实例和数组 | JVM 启动到结束 |
| 方法区(Method Area) | 是 | 类信息、常量、静态变量、JIT 代码缓存 | JVM 启动到结束 |
| 虚拟机栈(VM Stack) | 否 | 栈帧(局部变量表、操作数栈、动态链接、返回地址) | 与线程同生灭 |
| 本地方法栈(Native Stack) | 否 | 本地方法调用 | 与线程同生灭 |
| 程序计数器(PC Register) | 否 | 当前线程执行字节码的行号指示器 | 与线程同生灭 |
JDK 8 方法区变化: 方法区在 HotSpot 中的实现是"永久代"(PermGen),JDK 8 移除永久代,改用本地内存中的"元空间"(Metaspace)。
执行引擎(Execution Engine)
负责执行字节码。核心组件:
- 解释器(Interpreter):逐条读取字节码解释执行,启动快但运行慢
- JIT 编译器(Just-In-Time Compiler):将热点代码编译为本地机器码,编译慢但运行快
- 垃圾回收器(GC):自动回收不再使用的对象
HotSpot JVM 深入
HotSpot 是 OpenJDK 的默认 JVM 实现,名称来源于其"热点代码探测"机制。
解释执行 vs 编译执行
程序启动阶段:
.class 字节码 --> 解释器逐条执行 --> 运行结果
|
检测热点代码
|
v
JIT 编译器编译为本地机器码
|
v
后续调用直接执行本地机器码(极快)解释执行:逐条将字节码翻译为机器码执行,每次执行都需要翻译,但不需要等待编译时间。
编译执行:将字节码一次性编译为本地机器码,首次编译有开销,但后续执行直接使用机器码,效率极高。
JIT 编译器:C1 和 C2
HotSpot 内置两个 JIT 编译器:
| 编译器 | 场景 | 特点 |
|---|---|---|
| C1(Client Compiler) | 桌面应用 | 编译速度快,优化程度较低,适合对启动时间敏感的场景 |
| C2(Server Compiler) | 服务端应用 | 编译速度慢,但优化程度高,运行时性能更好 |
分层编译(Tiered Compilation):JDK 7 开始引入,是 C1 和 C2 的混合模式:
Level 0: 解释执行(不编译,收集 profiling 数据)
Level 1: C1 编译,无 profiling
Level 2: C1 编译,带基础 profiling
Level 3: C1 编译,带完整 profiling
Level 4: C2 编译,基于 profiling 数据进行深度优化默认情况下,热点代码从 Level 0 逐步升级到 Level 4,平衡了启动速度和峰值性能。
JIT 编译优化技术
1. 方法内联(Method Inlining)
将热点方法调用替换为方法体本身,减少方法调用开销:
java
// 编译前
public int compute(int a, int b) {
return add(a, b) * 2;
}
private int add(int x, int y) {
return x + y;
}
// JIT 内联后(等价于)
public int compute(int a, int b) {
return (a + b) * 2;
}2. 逃逸分析(Escape Analysis)
分析对象的作用域,如果对象不会逃逸出方法或线程,可以进行优化:
java
// 对象不会逃逸出方法 → 可以栈上分配
public void process() {
Point p = new Point(1, 2); // p 不会逃逸 → 可能在栈上分配,无需 GC
int result = p.x + p.y;
}
// 标量替换:将对象拆分为基本类型成员
// 等价于:
public void process() {
int x = 1;
int y = 2;
int result = x + y;
}3. 锁消除(Lock Elision)
java
// 同步块中的对象不会逃逸,锁可以被消除
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸
sb.append(a); // 内部有 synchronized
sb.append(b); // 锁可以被消除
return sb.toString();
}4. 栈上分配(Stack Allocation)
经过逃逸分析,如果对象不会逃逸出方法,可以将其分配在栈上而非堆上,方法结束自动销毁,无需 GC 介入。
查看 JIT 编译信息
bash
# 打印编译信息
java -XX:+PrintCompilation MyApp
# 输出示例:
# 70 1 3 java.lang.String::hashCode (55 bytes)
# 71 2 3 java.lang.String::charAt (29 bytes)
# 72 3 4 java.lang.String::hashCode (55 bytes) -- 被 C2 重新编译- 第一列:编译ID(毫秒时间戳)
- 第二列:编译ID
- 第三列:编译层级(3=C1,4=C2)
- 后面:方法名和字节码大小
代码示例:观察 JVM 行为
1. 观察类加载
java
public class ClassLoadingDemo {
public static void main(String[] args) {
// 打印各个类加载器
System.out.println("String 的类加载器: "
+ String.class.getClassLoader()); // null (Bootstrap)
System.out.println("应用程序类加载器: "
+ ClassLoadingDemo.class.getClassLoader()); // AppClassLoader
System.out.println("应用类加载器的父加载器: "
+ ClassLoadingDemo.class.getClassLoader().getParent()); // PlatformClassLoader
System.out.println("平台类加载器的父加载器: "
+ ClassLoadingDemo.class.getClassLoader().getParent().getParent()); // null
}
}2. 观察内存区域
java
/**
* VM Args: -Xms20m -Xmx20m -XX:+PrintGCDetails
*/
public class MemoryAreaDemo {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
// 堆内存分配
byte[] allocation1 = new byte[2 * _1MB];
byte[] allocation2 = new byte[2 * _1MB];
byte[] allocation3 = new byte[2 * _1MB];
byte[] allocation4 = new byte[4 * _1MB]; // 触发 GC
System.out.println("程序运行结束");
}
}3. 观察 JIT 优化效果
java
/**
* VM Args: -XX:+PrintCompilation
*/
public class JITDemo {
public static void main(String[] args) {
for (int i = 0; i < 10_000; i++) {
hotMethod(); // 被调用多次,触发 JIT 编译
}
}
private static int hotMethod() {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
}4. 观察逃逸分析
java
/**
* VM Args: -XX:+DoEscapeAnalysis -XX:+PrintEscapeAnalysis
* -XX:+EliminateAllocations
*/
public class EscapeAnalysisDemo {
public static void main(String[] args) {
long start = System.nanoTime();
for (int i = 0; i < 100_000_000; i++) {
alloc();
}
long end = System.nanoTime();
System.out.println("耗时: " + (end - start) / 1_000_000 + " ms");
}
private static void alloc() {
User user = new User();
user.id = 1;
user.name = "test";
// user 对象不会逃逸出 alloc() 方法
// 开启逃逸分析后,可能被栈上分配甚至标量替换
}
static class User {
int id;
String name;
}
}对比开启/关闭逃逸分析:
bash
# 开启逃逸分析(默认)
java -XX:+DoEscapeAnalysis EscapeAnalysisDemo
# 关闭逃逸分析
java -XX:-DoEscapeAnalysis EscapeAnalysisDemo常用 JVM 参数
| 参数 | 说明 |
|---|---|
-Xms | 初始堆大小 |
-Xmx | 最大堆大小 |
-Xss | 每个线程栈大小 |
-XX:MetaspaceSize | 元空间初始大小 |
-XX:MaxMetaspaceSize | 元空间最大大小 |
-XX:+PrintCompilation | 打印 JIT 编译信息 |
-XX:+PrintGCDetails | 打印 GC 详细信息 |
-XX:+UseG1GC | 使用 G1 垃圾回收器 |
-XX:+UseZGC | 使用 ZGC 垃圾回收器 |
-XX:+DoEscapeAnalysis | 开启逃逸分析(默认开启) |
-XX:+TieredCompilation | 开启分层编译(默认开启) |
面试高频问题
Q1: JVM 是如何实现"一次编写,到处运行"的?
A: Java 源码被 javac 编译为平台无关的字节码(.class 文件),字节码不包含任何平台相关的指令。JVM 充当了字节码和操作系统之间的桥梁,每种操作系统都有对应的 JVM 实现,JVM 负责将字节码翻译为当前平台的机器码执行,从而实现了跨平台。
Q2: JVM、JRE、JDK 有什么区别?
A: JVM 是 Java 虚拟机,负责执行字节码。JRE = JVM + 核心类库 + 其他运行支持,是 Java 运行时环境。JDK = JRE + 开发工具(javac, javap, jdb 等),是 Java 开发工具包。
Q3: 方法区(Method Area)和永久代(PermGen)是一回事吗?
A: 不是。方法区是 JVM 规范中的逻辑概念,永久代是 HotSpot 对方法区的一种实现。JDK 8 中永久代被移除,HotSpot 改用本地内存中的"元空间"(Metaspace)来实现方法区。元空间的好处是使用本地内存,默认不设置上限,不易发生 OOM。
Q4: 解释执行和 JIT 编译的区别是什么?
A: 解释执行是逐条翻译字节码为机器码执行,启动快但运行慢。JIT 编译是将热点代码一次性编译为本地机器码,编译有开销但后续执行极快。HotSpot 采用混合模式:启动时解释执行,探测到热点代码后触发 JIT 编译,实现"快速启动 + 高峰性能"的平衡。
Q5: 什么是分层编译?
A: 分层编译是 HotSpot 从 JDK 7 开始引入的优化策略。它将编译分为 5 个层级(Level 0-4):Level 0 是解释执行,Level 1-3 是 C1 编译,Level 4 是 C2 编译。热点代码逐步升级,C1 快速编译保证启动速度,C2 深度优化保证峰值性能。
Q6: 逃逸分析是什么?它能带来哪些优化?
A: 逃逸分析是 JIT 编译器的一种优化技术,分析对象的作用域。如果对象不会逃逸出方法或线程,可以进行:1)栈上分配(对象分配在栈上,方法结束自动释放);2)标量替换(将对象拆分为基本类型成员);3)锁消除(删除不必要的同步操作)。
Q7: 什么情况下 JVM 会退出?
A: JVM 在以下情况退出:1)所有非守护线程结束;2)某个线程调用 System.exit() 或 Runtime.halt();3)遇到未捕获的异常导致主线程退出;4)操作系统强制终止进程。
