【JVM深度解析】第01篇:JVM前世今生与技术架构全景
摘要
JVM(Java Virtual Machine,Java虚拟机)是Java生态体系的核心基础设施,也是"一次编写,到处运行"这一承诺的技术基石。本文从JVM的诞生背景出发,梳理其三十年发展历程,深入剖析HotSpot JVM的整体架构——包括类加载子系统、运行时数据区、执行引擎、垃圾回收器等核心组件的职责与协作关系,并解析字节码执行的完整生命周期。无论你是Java初学者还是希望夯实底层认知的中高级开发者,读完本文都能在脑海中建立起一张清晰完整的JVM全景地图,为后续深入每个模块打下扎实基础。
引言
你有没有想过,同样一份.class文件,为什么能在 Windows、Linux、macOS 上无差别运行?当你写下new Object()的那一刻,内存里究竟发生了什么?线上服务突然 Full GC 飙升、接口响应变慢,症结到底藏在哪里?
这些问题的答案,都藏在 JVM 里。
JVM 不是一个遥远的黑盒,而是每一个 Java/Kotlin/Scala 开发者每天都在打交道的"虚拟计算机"。理解它,意味着你能写出更高效的代码、做出更准确的性能判断、在排查故障时直指要害。
本系列共 32 篇文章,将系统地带你从 JVM 的基础原理一路走到高级特性、参数调优、并发编程,最终抵达 GraalVM 等新兴技术的前沿。第01篇是全系列的起点——我们先把整张地图铺开来看。
一、JVM 的前世今生
1.1 为什么需要虚拟机?
1990年代初期,互联网尚未普及,软件分发极度碎片化。C/C++ 程序需要针对不同操作系统、不同 CPU 架构分别编译,维护成本极高。Sun Microsystems 的工程师们意识到:如果能在操作系统之上再抽象一层"虚拟的计算机",让程序只和这个虚拟机打交道,跨平台问题就迎刃而解。
这个想法催生了 JVM。
1.2 Oak 到 Java:从机顶盒到互联网
1991年 ──→ Green 项目启动,James Gosling 主导 1992年 ──→ Oak 语言诞生(针对嵌入式设备/机顶盒) 1995年 ──→ 更名为 Java,HotJava 浏览器发布,震惊业界 1996年 ──→ JDK 1.0 正式发布,附带第一代 JVM(Classic VM) 1999年 ──→ HotSpot VM 发布(Sun 收购 Longview Technologies 获得) 2004年 ──→ Java 5,泛型/注解/枚举等重大语言特性加入 2006年 ──→ Java 开源,OpenJDK 项目成立 2009年 ──→ Oracle 收购 Sun,JVM 归属变更 2011年 ──→ Java 7,G1 GC 进入实验性支持 2014年 ──→ Java 8,Lambda + Stream + 默认方法,里程碑版本 2017年 ──→ Java 9,模块化系统(Project Jigsaw) 2018年 ──→ Java 11,ZGC 首次亮相(实验性) 2021年 ──→ Java 17,ZGC/Shenandoah 正式 GA,LTS 版本 2023年 ──→ Java 21,虚拟线程(Project Loom)正式发布 2024年 ──→ Java 23/24,持续演进中从最初为嵌入式设备而生,到支撑全球数十亿设备和海量后端服务,JVM 完成了历史上少有的技术跨越。
1.3 JVM 规范与实现:不只有 HotSpot
很多开发者以为 JVM 就是 HotSpot,其实JVM 是一套规范(由 JVM Specification 定义),任何人都可以按照规范实现自己的 JVM:
| JVM 实现 | 开发方 | 特点 |
|---|---|---|
| HotSpot | Oracle(原 Sun) | 最主流,生产首选,C2 编译器性能强 |
| OpenJ9 | Eclipse/IBM | 低内存占用,启动快,适合容器部署 |
| GraalVM | Oracle | 支持 AOT 编译,多语言运行时 |
| Zing (Azul) | Azul Systems | 超低停顿(C4 GC),商业版 |
| Android ART | 为移动设备优化,Dalvik 的继承者 | |
| Jikes RVM | IBM Research | 学术研究用途,Java 写的 JVM |
本系列以HotSpot JVM为主要讲解对象,它是绝大多数生产环境使用的 JVM 实现。
二、JVM 整体架构全景
理解 JVM,最好的方式是先看全局架构图,再逐一深入各个组件。
2.1 架构全景图
┌─────────────────────────────────────────────────────────────────┐ │ Java 源代码 (.java) │ └─────────────────────────────┬───────────────────────────────────┘ │ javac 编译 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 字节码文件 (.class) │ └─────────────────────────────┬───────────────────────────────────┘ │ ▼ ╔═════════════════════════════════════════════════════════════════╗ ║ JVM(以 HotSpot 为例) ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ║ ║ │ 类加载子系统 (Class Loader Subsystem) │ ║ ║ │ Bootstrap CL → Extension CL → Application CL → 自定义CL │ ║ ║ │ 加载 → 验证 → 准备 → 解析 → 初始化 │ ║ ║ └───────────────────────┬─────────────────────────────────┘ ║ ║ │ 加载到内存 ║ ║ ▼ ║ ║ ┌─────────────────────────────────────────────────────────┐ ║ ║ │ 运行时数据区 (Runtime Data Areas) │ ║ ║ │ │ ║ ║ │ ┌─────────────────┐ ┌──────────────────────────────┐ │ ║ ║ │ │ 方法区 │ │ 堆 (Heap) │ │ ║ ║ │ │ (Method Area) │ │ ┌──────────┐ ┌──────────┐ │ │ ║ ║ │ │ 类信息/常量池 │ │ │ 年轻代 │ │ 老年代 │ │ │ ║ ║ │ │ 静态变量 │ │ │(Young) │ │ (Old) │ │ │ ║ ║ │ └─────────────────┘ │ └──────────┘ └──────────┘ │ │ ║ ║ │ └──────────────────────────────┘ │ ║ ║ │ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │ ║ ║ │ │ Java 虚拟机 │ │ 本地方法栈 │ │ 程序计数器 │ │ ║ ║ │ │ 栈 (Stack) │ │(Native Stack) │ │ (PC) │ │ ║ ║ │ │ 栈帧/局部变量 │ │ JNI调用 │ │ 当前指令 │ │ ║ ║ │ └──────────────┘ └────────────────┘ └─────────────┘ │ ║ ║ └───────────────────────┬─────────────────────────────────┘ ║ ║ │ ║ ║ ▼ ║ ║ ┌─────────────────────────────────────────────────────────┐ ║ ║ │ 执行引擎 (Execution Engine) │ ║ ║ │ │ ║ ║ │ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │ ║ ║ │ │ 解释器 │ │ JIT 编译器 │ │ 垃圾回收器 │ │ ║ ║ │ │(Interpreter) │ │ (C1 Tier1 + │ │ (GC) │ │ ║ ║ │ │ 逐条解释执行 │ │ C2 Tier4) │ │ 自动内存管理 │ │ ║ ║ │ └──────────────┘ └────────────────┘ └─────────────┘ │ ║ ║ └─────────────────────────────────────────────────────────┘ ║ ║ ║ ║ ┌─────────────────────────────────────────────────────────┐ ║ ║ │ 本地方法接口 (JNI) + 本地方法库 │ ║ ║ └─────────────────────────────────────────────────────────┘ ║ ╚═════════════════════════════════════════════════════════════════╝ │ ▼ 操作系统 (OS) / 硬件📌图注:JVM 架构可以理解为三大主干——类加载负责把字节码搬进内存,运行时数据区负责组织内存空间,执行引擎负责实际运行代码。
2.2 各组件职责一览
| 组件 | 核心职责 | 关键特性 |
|---|---|---|
| 类加载子系统 | 将 .class 文件加载到 JVM | 双亲委派模型、懒加载 |
| 方法区 | 存储类元信息、常量池、静态变量 | JDK8+ 改为元空间(Metaspace) |
| 堆(Heap) | 存储所有对象实例和数组 | GC 管理区域,最大的内存区域 |
| 虚拟机栈 | 每个线程独有,存储栈帧 | 线程私有,递归过深→StackOverflowError |
| 本地方法栈 | 调用 Native 方法时使用 | HotSpot 中与虚拟机栈合并实现 |
| 程序计数器 | 记录当前线程执行到的字节码行号 | 唯一不会 OOM 的区域 |
| 执行引擎 | 解释/编译执行字节码 | 解释器 + JIT 双模混合执行 |
| JNI | 调用 C/C++ 本地库 | Java 与 Native 的桥梁 |
三、JVM 的核心工作流程
3.1 从源码到执行的完整链路
一段 Java 代码从编写到运行,要经历以下阶段:
① 编写源代码 (HelloWorld.java) │ ▼ ② javac 前端编译 ├── 词法分析(Lexical Analysis) ├── 语法分析(Syntax Analysis) ├── 语义分析(Semantic Analysis) ├── 字节码生成 └── 生成 HelloWorld.class │ ▼ ③ JVM 类加载(Class Loading) ├── 加载(Loading):读取 .class 文件 ├── 验证(Verification):字节码合法性检查 ├── 准备(Preparation):静态变量分配内存并赋零值 ├── 解析(Resolution):符号引用→直接引用 └── 初始化(Initialization):执行 <clinit>() │ ▼ ④ 执行引擎运行 ├── 解释器:首次运行,逐条解释字节码 ├── C1 编译器(Tier1-3):热点方法编译,轻量优化 └── C2 编译器(Tier4):超热点方法,激进优化(内联、逃逸分析) │ ▼ ⑤ 运行时内存管理 ├── 对象分配(TLAB 快速分配 / 指针碰撞) ├── Minor GC(年轻代回收,Stop-The-World 短暂) └── Major/Full GC(老年代/全堆回收)3.2 解释执行 vs JIT 编译:混合执行模式
HotSpot JVM 使用**分层编译(Tiered Compilation)**策略,将解释执行和 JIT 编译有机结合:
调用次数 │ │ 10000次阈值(默认) │─────────────────────────── C2 编译(最高优化) │ │ 1500次阈值(默认) │─────────────────────────── C1 编译(轻量优化) │ │ 0次(首次调用) └─────────────────────────── 解释器执行| 执行层级 | 触发条件 | 优化程度 | 延迟 |
|---|---|---|---|
| Tier 0:解释器 | 首次调用 | 无优化 | 最低 |
| Tier 1-3:C1 | 调用次数达阈值 | 基础优化 | 较快 |
| Tier 4:C2 | 超热点代码 | 激进优化(内联/逃逸分析) | 最优 |
工程意义:这就是为什么 JVM 程序会有"预热(Warm-up)"现象——刚启动时较慢,运行一段时间后性能大幅提升。在高性能场景,合理配置预热策略非常重要。
3.3 内存分配的快车道:TLAB
每次新建对象,JVM 不是直接在堆上竞争式分配,而是给每个线程预先在 Eden 区划分一块线程本地分配缓冲区(TLAB,Thread-Local Allocation Buffer):
堆 (Heap) ├── 年轻代 (Young Generation) │ ├── Eden 区 │ │ ├── Thread-1 的 TLAB ← new Object() 优先在此分配 │ │ ├── Thread-2 的 TLAB │ │ └── Thread-N 的 TLAB │ ├── Survivor-0 (S0) │ └── Survivor-1 (S1) └── 老年代 (Old Generation)TLAB 分配无需加锁,极大提升了多线程下对象创建的吞吐量。当 TLAB 满了,才会触发慢速路径(加锁在公共 Eden 区分配)。
四、运行时数据区详解
4.1 堆(Heap):最大的内存区域
堆是 JVM 中最重要的内存区域,几乎所有的对象实例都在这里分配(栈上分配和 TLAB 是特殊情况)。
堆的内存布局(G1 之前的经典分代模型):
┌─────────────────────────────────────────────────────┐ │ 堆内存 (Heap) │ │ │ │ ┌──────────────────────────────┐ ┌──────────────┐ │ │ │ 年轻代 (Young Gen) │ │ 老年代 │ │ │ │ │ │ (Old Gen) │ │ │ │ ┌────────┐ ┌────┐ ┌────┐ │ │ │ │ │ │ │ Eden │ │ S0 │ │ S1 │ │ │ 长期存活对象 │ │ │ │ │ 新对象 │ │ │ │ │ │ │ 大对象 │ │ │ │ └────────┘ └────┘ └────┘ │ │ │ │ │ │ 8 : 1 : 1 默认比例 │ │ │ │ │ └──────────────────────────────┘ └──────────────┘ │ │ (-Xmn 控制) (老年代=Xmx-Xmn) │ └─────────────────────────────────────────────────────┘关键参数:
-Xms2g# 堆初始大小 2GB-Xmx4g# 堆最大大小 4GB(建议与Xms相同,避免动态扩容)-Xmn1g# 年轻代大小 1GB-XX:NewRatio=2# 老年代:年轻代 = 2:1(与-Xmn二选一)4.2 方法区(Method Area)/ 元空间(Metaspace)
方法区存储类的元信息,包括:
- 类的全限定名、父类信息、接口列表
- 字段和方法的描述符
- 运行时常量池(字符串字面量、符号引用等)
- 静态变量
- 方法的字节码、异常表
JDK 演进关键变化:
JDK 1.7 及以前:永久代 (PermGen) ├── 在堆内分配 ├── -XX:MaxPermSize=256m 控制大小 └── 容易出现 java.lang.OutOfMemoryError: PermGen space JDK 1.8+:元空间 (Metaspace) ├── 迁移到本地内存(Native Memory),不受堆限制 ├── -XX:MaxMetaspaceSize=256m 控制上限(不设则默认无限) └── 大幅减少了 PermGen OOM 问题4.3 虚拟机栈(VM Stack)
每个线程拥有独立的虚拟机栈,每次方法调用都会创建一个栈帧(Stack Frame):
线程 A 的虚拟机栈 ┌─────────────────────────────────┐ │ 栈帧:methodC() │ ← 当前执行方法(栈顶) │ 局部变量表 | 操作数栈 | 动态链接 │ ├─────────────────────────────────┤ │ 栈帧:methodB() │ ├─────────────────────────────────┤ │ 栈帧:methodA() │ ├─────────────────────────────────┤ │ 栈帧:main() │ ← 栈底 └─────────────────────────────────┘栈帧的四大组成部分:
- 局部变量表:存放方法参数和局部变量(基本类型直接存值,引用类型存对象地址)
- 操作数栈:方法执行时的工作区,字节码指令的操作数在此入栈出栈
- 动态连接:指向运行时常量池中本方法的符号引用
- 方法返回地址:方法退出后回到调用者的位置
常见异常:
StackOverflowError:栈深度超过-Xss限制(无限递归)OutOfMemoryError:动态扩展栈时内存不足
4.4 程序计数器(PC Register)
程序计数器是 JVM 中最小的内存区域,也是唯一不会发生 OOM 的区域。
- 每个线程独有,保存当前正在执行的字节码指令的行号(偏移量)
- 执行 Native 方法时值为 Undefined
- CPU 在多线程切换时依靠 PC 恢复现场
五、执行引擎:让字节码"活"起来
5.1 解释器(Interpreter)
解释器逐条读取字节码指令并执行,优点是启动快,缺点是执行效率低。HotSpot 中的解释器是模板解释器(Template Interpreter),为每条字节码指令预生成了对应的本地机器码片段,比纯软件解释快很多。
5.2 JIT 编译器
C1 编译器(Client Compiler):
- 注重启动速度,优化较简单
- 执行简单的内联、方法内联、去虚化等优化
- 适合对延迟敏感的客户端应用
C2 编译器(Server Compiler):
- 注重峰值吞吐量,优化激进耗时
- 逃逸分析、标量替换、锁消除、循环展开等高级优化
- 适合长期运行的服务端应用
分层编译(Tiered Compilation,JDK 8 默认启用):
C1 和 C2 协同工作,程序先用 C1 快速编译获得基础性能提升,运行一段时间后超热点代码再被 C2 重新编译获得最优性能。
5.3 垃圾收集器(GC)
垃圾收集器是执行引擎的重要组成部分,负责自动管理堆内存:
JVM 垃圾收集器演进路线图 Serial GC ──────────────────────────────────→ │ (单线程,简单,适合小堆) ▼ Parallel GC ─────────────────────────────────→ │ (多线程,高吞吐量,JDK8默认) ▼ CMS GC ──────────────────────────────────────→ │ (并发标记清除,低停顿,JDK9废弃) ▼ G1 GC ───────────────────────────────────────→ │ (Region分代,可预测停顿,JDK9默认) ▼ ZGC ─────────────────────────────────────────→ │ (超低停顿<10ms,JDK15 GA) ▼ Shenandoah GC ───────────────────────────────→ (RedHat研发,并发整理,JDK15 GA)本系列将用多篇文章深入讲解各 GC 的工作原理,这里先有个整体印象。
六、JVM 规范与实现的边界
理解 JVM 规范和具体实现的区别,是避免"对号入座"错误认知的关键:
| 内容 | 规范(Specification)要求 | HotSpot 实现 |
|---|---|---|
| 运行时数据区划分 | 规定各区域的逻辑概念 | 具体内存布局由实现决定 |
| 方法区 | 逻辑概念 | JDK8 前=PermGen,JDK8+=Metaspace |
| 垃圾回收 | 规范完全不要求! | HotSpot 自行实现多种 GC |
| 本地方法栈 | 可以与虚拟机栈合并 | HotSpot 合并为一个栈 |
| 对象内存布局 | 不规定 | 对象头+实例数据+对齐填充 |
七、写给开发者的实践建议
理解 JVM 架构不是为了炫技,而是要能指导日常开发和线上问题排查:
1. 对象"活在哪里"决定了 GC 的压力
理解堆的分代模型,就能明白为什么大量短命对象(如频繁创建的临时 VO)会导致 Minor GC 频繁,而大对象会直接进老年代增加 Full GC 风险。
2. 栈帧告诉你递归的边界
知道每个方法调用都会占用栈帧,就能理解-Xss参数的意义,也能预判深度递归的StackOverflowError。
3. 分层编译解释了"预热"现象
服务刚启动时吞吐量低,这不是 Bug,是 JVM 的正常行为。对于高并发服务,上线前应做好预热(接口探活、流量引导等)。
4. 元空间 OOM 的根源往往是类加载泄漏
频繁动态生成类(如 CGLib 代理、Groovy 脚本热加载)未正确卸载,会导致元空间耗尽。
八、总结
本文带你完整浏览了 JVM 的全景地图:
- 历史脉络:从 1991 年的 Oak 语言到今天的 Java 21,JVM 完成了从嵌入式到互联网再到云原生的三次蜕变
- 架构全景:类加载子系统 → 运行时数据区 → 执行引擎,三大主干各司其职
- 运行时数据区:堆(对象)、方法区/元空间(类信息)、虚拟机栈(方法调用)、程序计数器(指令追踪)
- 执行引擎:解释器+JIT(C1/C2)的混合执行模式,分层编译平衡启动速度与峰值性能
- GC 演进:Serial → Parallel → CMS → G1 → ZGC/Shenandoah,停顿时间不断缩短
一句话记住 JVM 的本质:JVM 是一台虚拟的计算机,它有自己的指令集(字节码)、内存模型(堆/栈/方法区)和执行引擎(解释+JIT),运行在真实操作系统之上,对上屏蔽了平台差异,对下利用 JIT 和 GC 极致榨取硬件性能。
系列导航
- 下一篇:【JVM深度解析】第02篇:类加载机制深度解析
- 系列目录:JVM深度解析
参考资料
- 《深入理解Java虚拟机(第3版)》— 周志明著,机械工业出版社
- JVM Specification(Java SE 21)
- HotSpot JVM 架构概览
- OpenJDK 源码仓库
- Java Platform, Standard Edition HotSpot Virtual Machine GC Tuning Guide
- Aleksey Shipilёv: JVM Anatomy Quarks
