引言
为什么要学习这个主题?
在前两篇文章中,我们知道了Java程序如何运行,以及JVM的本质。但JVM内部到底是怎么工作的?
想象一下:
- 一个
.class文件是如何被加载到JVM中的? - 对象和变量存储在哪里?
- 字节码是如何被"翻译"成机器码的?
理解JVM的架构,就像理解一台计算机的组成(CPU、内存、硬盘)一样重要。这是后续学习类加载、内存管理、GC调优的基础。
你将学到什么?
- ✅ JVM的整体架构图
- ✅ 五大核心组件的职责
- ✅ 各组件如何协作运行Java程序
- ✅ JVM的完整执行流程
一、JVM架构全景图
1.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ Java应用程序 │
│ (.java → .class) │
└──────────────────────┬──────────────────────────────────────┘
│ 字节码
↓
┌─────────────────────────────────────────────────────────────┐
│ JVM(Java虚拟机) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1️⃣ 类加载子系统 (Class Loader Subsystem) │ │
│ │ - 加载 (Loading) │ │
│ │ - 链接 (Linking): 验证、准备、解析 │ │
│ │ - 初始化 (Initialization) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 2️⃣ 运行时数据区 (Runtime Data Areas) │ │
│ │ │ │
│ │ 【线程共享】 【线程私有】 │ │
│ │ - 堆 (Heap) - 程序计数器 (PC Register)│ │
│ │ - 方法区 (Method Area) - 虚拟机栈 (VM Stack) │ │
│ │ - 本地方法栈 (Native Stack)│ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 3️⃣ 执行引擎 (Execution Engine) │ │
│ │ - 解释器 (Interpreter) │ │
│ │ - JIT编译器 (Just-In-Time Compiler) │ │
│ │ - 垃圾收集器 (Garbage Collector) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 4️⃣ 本地接口 (Native Interface - JNI) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 5️⃣ 本地方法库 (Native Method Libraries) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 操作系统 & 硬件 │
└─────────────────────────────────────────────────────────────┘
1.2 核心组件概览
| 组件 | 作用 | 类比 |
|---|---|---|
| 类加载子系统 | 加载、链接、初始化类 | 快递员(把包裹送到仓库) |
| 运行时数据区 | 存储数据(对象、变量、代码) | 内存/仓库 |
| 执行引擎 | 执行字节码 | CPU(执行指令) |
| 本地接口 | 调用操作系统API | 操作系统接口 |
| 垃圾收集器 | 自动回收无用对象 | 清洁工(清理垃圾) |
二、类加载子系统(Class Loader Subsystem)
2.1 职责
负责将.class文件加载到JVM内存中,并准备好供使用。
2.2 三个阶段
.class文件
↓
1️⃣ Loading(加载)
↓
2️⃣ Linking(链接)
- Verification(验证)
- Preparation(准备)
- Resolution(解析)
↓
3️⃣ Initialization(初始化)
↓
运行时数据区(可以使用了)
加载(Loading)
动作:
- 通过类的全限定名找到
.class文件 - 读取字节码数据
- 在堆中生成一个
Class对象
示例:
// 触发加载
Class<?> clazz = Class.forName("com.example.User");
链接(Linking)
1. 验证(Verification):
- 检查字节码是否符合JVM规范
- 防止恶意代码
2. 准备(Preparation):
- 为静态变量分配内存
- 设置默认值
示例:
public class Example {
private static int count = 100; // 准备阶段:count = 0
// 初始化阶段:count = 100
}
3. 解析(Resolution):
- 将符号引用转换为直接引用
- 符号引用:字符串形式(如"com.example.User")
- 直接引用:内存地址
初始化(Initialization)
动作:
- 执行类构造器
<clinit>()方法 - 执行静态变量赋值
- 执行静态代码块
示例:
public class Example {
private static int count = 100; // ← 此时执行
static {
System.out.println("静态代码块执行"); // ← 此时执行
}
}
2.3 类加载器层次
┌────────────────────────┐
│ 启动类加载器 │ 加载核心类库(rt.jar)
│ Bootstrap ClassLoader │ 如:java.lang.*, java.util.*
└──────────┬─────────────┘
│ 父加载器
┌──────────▼─────────────┐
│ 扩展类加载器 │ 加载扩展库(ext目录)
│ Extension ClassLoader │ 如:javax.*
└──────────┬─────────────┘
│ 父加载器
┌──────────▼─────────────┐
│ 应用类加载器 │ 加载应用类(classpath)
│ Application ClassLoader│ 如:自己写的类
└────────────────────────┘
双亲委派模型:
- 先让父加载器尝试加载
- 父加载器无法加载时,子加载器才尝试
- 保证核心类不会被篡改
后续章节会详细讲解,这里先了解即可。
三、运行时数据区(Runtime Data Areas)
3.1 整体布局
┌─────────────────────────────────────────────┐
│ 运行时数据区 │
│ │
│ ┌────────────────────┐ ┌────────────────┐│
│ │ 方法区 (Method) │ │ 堆 (Heap) ││ 【线程共享】
│ │ - 类信息 │ │ - 对象实例 ││ 所有线程都能访问
│ │ - 静态变量 │ │ - 数组 ││
│ │ - 常量池 │ │ ││
│ └────────────────────┘ └────────────────┘│
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐│
│ │程序计数器│ │虚拟机栈 │ │本地方法栈 ││ 【线程私有】
│ │PC Register│ │VM Stack │ │Native Stack ││ 每个线程独有
│ └──────────┘ └──────────┘ └──────────────┘│
└─────────────────────────────────────────────┘
3.2 各区域详解
程序计数器(Program Counter Register)
作用:记录当前线程执行到哪条字节码指令
特点:
- 最小的内存区域
- 线程私有
- 唯一不会OOM的区域
类比:书签(记录读到第几页)
虚拟机栈(VM Stack)
作用:存储方法执行时的局部变量、操作数栈、方法出口等
结构:
┌─────────────────┐
│ 栈帧3(栈顶) │ ← 当前正在执行的方法
├─────────────────┤
│ 栈帧2 │ ← 调用者
├─────────────────┤
│ 栈帧1 │ ← main方法
└─────────────────┘
每个栈帧包含:
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
异常:
StackOverflowError:递归调用过深OutOfMemoryError:无法分配新栈
本地方法栈(Native Method Stack)
作用:为Native方法服务(用C/C++写的方法)
示例:
// JDK中的native方法
public native int hashCode();
堆(Heap)
作用:存储对象实例和数组
特点:
- 最大的内存区域
- 所有线程共享
- 垃圾收集器的主要管理区域
结构(简化版):
┌───────────────────────────┐
│ 新生代 (Young) │ 新创建的对象
│ - Eden │
│ - Survivor 0 │
│ - Survivor 1 │
├───────────────────────────┤
│ 老年代 (Old) │ 长期存活的对象
└───────────────────────────┘
示例:
User user = new User(); // user变量在栈,User对象在堆
方法区(Method Area)
作用:存储类信息、静态变量、常量池
存储内容:
- 类的结构信息(字段、方法、构造器)
- 运行时常量池
- 静态变量
演变:
- JDK 7:永久代(PermGen)
- JDK 8+:元空间(Metaspace)
区别:
永久代:在堆内存中,有大小限制
元空间:在本地内存中,默认无限制(受操作系统限制)
四、执行引擎(Execution Engine)
4.1 职责
将字节码转换为机器码并执行。
4.2 三种执行方式
字节码
↓
┌───┴────────────────┐
│ │
▼ ▼
解释执行 JIT编译
(逐行翻译) (一次编译,缓存)
↓ ↓
机器码 机器码
↓ ↓
CPU执行 CPU执行
解释器(Interpreter)
特点:
- 逐行解释执行字节码
- 启动快,执行慢
- 适合初次执行的代码
JIT编译器(Just-In-Time Compiler)
特点:
- 将热点代码编译成机器码
- 启动慢,执行快
- 适合频繁执行的代码
HotSpot的JIT:
- C1编译器:客户端编译器,编译速度快
- C2编译器:服务端编译器,优化程度深
分层编译(Tiered Compilation):
0层:解释执行
↓
1层:C1编译(简单优化)
↓
2层:C1编译(带profiling)
↓
3层:C1编译(完全优化)
↓
4层:C2编译(深度优化)
垃圾收集器(Garbage Collector)
作用:自动回收堆中无用的对象
后续会有专门章节详细讲解。
五、本地接口 & 本地方法库
5.1 本地接口(JNI - Java Native Interface)
作用:让Java代码调用C/C++代码(或被调用)
使用场景:
- 调用操作系统API
- 复用现有C/C++库
- 性能关键的代码
示例:
public class NativeDemo {
// 声明native方法
public native void hello();
static {
// 加载动态库
System.loadLibrary("native-lib");
}
}
5.2 本地方法库
包含:
- C/C++编写的动态库(.so、.dll、.dylib)
- JVM底层实现(文件IO、网络、线程等)
六、完整执行流程示例
6.1 代码示例
public class Example {
private static int count = 10;
public static void main(String[] args) {
User user = new User("Alice");
user.sayHello();
}
}
class User {
private String name;
public User(String name) {
this.name = name;
}
public void sayHello() {
System.out.println("Hello, " + name);
}
}
6.2 执行流程
1️⃣ 类加载阶段
- 加载Example.class到方法区
- 加载User.class到方法区
- 初始化:count = 10
2️⃣ main方法执行
- 在虚拟机栈创建main方法的栈帧
- PC寄存器指向main方法的第一条指令
3️⃣ 创建User对象
User user = new User("Alice");
① 在堆中分配User对象的内存
② 初始化对象(调用构造器)
③ 将堆中对象的地址赋值给栈中的user变量
【内存布局】
栈:user变量 → 0x1234(堆地址)
堆:0x1234 → User对象{name: "Alice"}
4️⃣ 调用方法
user.sayHello();
① 在虚拟机栈创建sayHello方法的栈帧
② 执行方法体(字节码)
③ 解释器执行或JIT编译执行
④ 调用System.out.println(native方法)
⑤ 通过JNI调用操作系统API
⑥ 输出到控制台
5️⃣ 方法返回
- 弹出sayHello栈帧
- 弹出main栈帧
- 程序结束
6️⃣ 垃圾回收
- User对象不再被引用
- GC自动回收堆中的对象
七、实战:观察JVM运行时数据
7.1 查看堆内存使用情况
public class MemoryInfo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
// 获取内存信息(单位:字节)
long maxMemory = runtime.maxMemory(); // 最大堆内存
long totalMemory = runtime.totalMemory(); // 已分配堆内存
long freeMemory = runtime.freeMemory(); // 空闲堆内存
System.out.println("最大堆内存: " + (maxMemory / 1024 / 1024) + "MB");
System.out.println("已分配堆内存: " + (totalMemory / 1024 / 1024) + "MB");
System.out.println("空闲堆内存: " + (freeMemory / 1024 / 1024) + "MB");
System.out.println("已使用堆内存: " +
((totalMemory - freeMemory) / 1024 / 1024) + "MB");
}
}
输出示例:
最大堆内存: 4096MB
已分配堆内存: 256MB
空闲堆内存: 200MB
已使用堆内存: 56MB
7.2 模拟StackOverflowError
public class StackOverflowDemo {
private static int depth = 0;
public static void recursion() {
depth++;
recursion(); // 无限递归
}
public static void main(String[] args) {
try {
recursion();
} catch (StackOverflowError e) {
System.out.println("栈深度: " + depth);
e.printStackTrace();
}
}
}
输出:
栈深度: 15532
java.lang.StackOverflowError
说明:每次方法调用都会在栈上创建栈帧,递归过深导致栈溢出。
总结
核心要点回顾
JVM五大核心组件
- 类加载子系统:加载、链接、初始化类
- 运行时数据区:存储数据的内存结构
- 执行引擎:执行字节码(解释 + JIT)
- 本地接口:调用C/C++代码
- 垃圾收集器:自动回收对象
运行时数据区
- 线程共享:堆、方法区
- 线程私有:程序计数器、虚拟机栈、本地方法栈
执行流程
- 类加载 → 内存分配 → 方法执行 → 垃圾回收
关键概念
- 栈:存储方法调用和局部变量
- 堆:存储对象实例
- 方法区:存储类信息
思考题
- 为什么局部变量存储在栈中,而对象存储在堆中?
- 如果一个方法内创建了100万个对象,会导致StackOverflowError还是OutOfMemoryError?
- 静态变量存储在哪里?为什么?
下一篇预告
下一篇《字节码是什么?从.java到.class的编译过程》,我们将深入探讨:
- 字节码的格式和结构
- javac编译器的工作原理
- 常量池、方法表、字节码指令
- 如何手工解读字节码
参考资料
- The Java Virtual Machine Specification - Chapter 2: The Structure of the Java Virtual Machine
- 《深入理解Java虚拟机(第3版)》- 周志明,第2章
- JVM Internals
欢迎在评论区分享你对JVM架构的理解!