JVM 的位置与整体结构
先从一张全景图说起
JVM 不是一个孤立的组件,它运行在操作系统之上,承载着 Java 应用程序。理解 JVM 在整个体系中的位置,是建立全局观的第一步。
┌─────────────────────────────────────────────────────────────┐
│ Java 应用程序 │
│ Spring Boot / MyBatis / Kafka / ... │
├─────────────────────────────────────────────────────────────┤
│ Java 核心类库 │
│ java.lang / java.util / java.io / java.nio │
├─────────────────────────────────────────────────────────────┤
│ JVM 运行时 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 类加载器 │ │ 执行引擎 │ │ 运行时数据区 │ │ 本地接口 │ │
│ │ (Class │ │(Execution │ │ (Runtime │ │(Native │ │
│ │ Loader) │ │ Engine) │ │ Data Area)│ │ Interface)│ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 操作系统 │
│ Linux / Windows / macOS │
├─────────────────────────────────────────────────────────────┤
│ 硬件 │
│ x86 / ARM / RISC-V │
└─────────────────────────────────────────────────────────────┘这张图里 JVM 的四大核心模块,每个都值得单独成篇来深入讲解。但在这一节,我们先建立一个整体的印象。
类加载器:谁来把字节码弄进来
当程序 new Student() 时,JVM 需要知道 Student.class 在哪里、怎么加载、加载后的类信息存在哪里。这个职责由类加载器(ClassLoader) 承担。
类加载器的工作不是简单地把文件读入内存,而是涉及一系列复杂的过程:
.class 字节码文件
│
▼
加载(Loading)→ 验证(Verification)→ 准备(Preparation)
│
▼
解析(Resolution)→ 初始化(Initialization)
│
▼
可用的 Class 对象进入 JVMJava 的类加载器是分层的:引导类加载器(Bootstrap)→ 扩展类加载器(Extension)→ 应用类加载器(Application)→ 自定义类加载器。它们之间形成了双亲委派模型,保证了类的唯一性和安全性。
关于类加载器的详细内容,我们会在后续章节深入讲解。
执行引擎:怎么把字节码跑起来
字节码被加载进来后,由执行引擎(Execution Engine) 负责执行。
执行引擎面临一个问题:字节码是 JVM 定义的一种「虚拟指令」,CPU 不认识它。执行引擎要做的事情,就是把字节码翻译成本地机器指令。
有两种翻译方式:
1. 解释执行
逐行解释字节码,翻译一条执行一条。启动快,但执行效率低。
字节码 → [解释器] → 机器码 → 执行2. JIT 编译
把整个方法(或一段代码)编译成本地机器码,以后直接执行编译后的代码。启动稍慢,但执行速度快很多。
字节码 → [JIT 编译器] → 本地机器码 → 缓存 → 执行HotSpot 默认采用解释+JIT 混合模式:程序启动时用解释执行,随着运行收集热点数据,JIT 编译器把热点代码编译成本地码执行。越跑越快,就是这个道理。
3. AOT 编译(JDK 9+)
Ahead-of-Time 编译,在运行前直接把字节码编译成本地码。启动快,但缺少了 JIT 的运行时优化能力。
运行时数据区:JVM 的「内存地图」
这是 JVM 中最核心的部分。Java 程序运行时,所有的数据都存在这里。
┌──────────────────────────────────────────────────────────────┐
│ 运行时数据区 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 线程私有区域 │ │ 线程共享区域 │ │
│ │ │ │ │ │
│ │ ┌────────────┐ │ │ ┌────────────────────┐ │ │
│ │ │ PC 寄存器 │ │ │ │ 堆(Heap) │ │ │
│ │ └────────────┘ │ │ │ 对象实例/数组 │ │ │
│ │ ┌────────────┐ │ │ └────────────────────┘ │ │
│ │ │ 虚拟机栈 │ │ │ ┌────────────────────┐ │ │
│ │ │ (VM Stack) │ │ │ │ 方法区 │ │ │
│ │ └────────────┘ │ │ │(Method Area/ │ │ │
│ │ ┌────────────┐ │ │ │ Metaspace) │ │ │
│ │ │ 本地方法栈 │ │ │ │ 类信息/常量/静态变量 │ │ │
│ │ │(Native │ │ │ └────────────────────┘ │ │
│ │ │Method Stk)│ │ │ ┌────────────────────┐ │ │
│ │ └────────────┘ │ │ │ 直接内存 │ │ │
│ └──────────────────┘ │ │ (Direct Memory) │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘线程私有区域(每个线程独有)
- PC 寄存器(Program Counter Register):记录当前线程正在执行哪条字节码指令
- 虚拟机栈(VM Stack):每个方法调用对应一个栈帧(Frame),存储局部变量、操作数栈、方法返回值
- 本地方法栈(Native Method Stack):为 native 方法服务
线程共享区域(所有线程共用)
- 堆(Heap):几乎所有对象实例和数组都存在这里,GC 的主要战场
- 方法区(Method Area):存储类信息(类名、修饰符、字段、方法)、常量、静态变量。JDK 8 后用 Metaspace 实现
- 直接内存(Direct Memory):NIO 使用的堆外内存,不受堆大小限制
关于运行时数据区的详细结构,我们会在后续章节逐一深入讲解。
本地接口:打通 Java 和 C/C++
Java 可以在代码中调用本地方法(用 native 关键字修饰的方法)。这些方法的实现不是 Java 代码,而是 C 或 C++ 代码。
JNI(Java Native Interface)就是 Java 和本地代码之间的桥梁:
public class System {
// 源码是 Java
public static long currentTimeMillis() {
return currentTimeMillis0();
}
// currentTimeMillis0 是一个 native 方法
// 实现在 JVM 内部调用的 C 代码中
private static native long currentTimeMillis0();
}System.currentTimeMillis() 底层调用的就是操作系统的时间函数。这是 JVM 依赖操作系统能力的方式之一。
JVM 和 JDK/JRE 的关系再梳理
有了 JVM 整体结构的认识,再来看 JDK/JRE/JVM 的关系就清晰多了:
JDK = Java 开发工具包
JRE = Java 运行时环境 = JVM + Java 核心类库
JVM = Java 虚拟机 = 只负责运行字节码开发阶段需要 JDK(javac 编译器在里面),生产环境部署只需要 JRE。如果只是运行一个 jar 包,java -jar 命令只依赖 JRE。
本节小结
JVM 的整体结构分为四大模块:
- 类加载器:把
.class文件加载进 JVM - 执行引擎:把字节码翻译成本地指令并执行
- 运行时数据区:程序运行时的内存布局(堆、栈、方法区……)
- 本地接口:让 Java 代码能调用操作系统底层能力
这是理解 JVM 的框架,后续所有章节都是在这个框架上添砖加瓦。
下一节,我们来看看 Java 代码执行流程,从 .java 文件到输出结果,JVM 到底经历了什么。
