【JVM深度解析】第01篇:JVM前世今生与技术架构全景
摘要JVMJava Virtual MachineJava虚拟机是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年 ──→ 更名为 JavaHotJava 浏览器发布震惊业界 1996年 ──→ JDK 1.0 正式发布附带第一代 JVMClassic VM 1999年 ──→ HotSpot VM 发布Sun 收购 Longview Technologies 获得 2004年 ──→ Java 5泛型/注解/枚举等重大语言特性加入 2006年 ──→ Java 开源OpenJDK 项目成立 2009年 ──→ Oracle 收购 SunJVM 归属变更 2011年 ──→ Java 7G1 GC 进入实验性支持 2014年 ──→ Java 8Lambda Stream 默认方法里程碑版本 2017年 ──→ Java 9模块化系统Project Jigsaw 2018年 ──→ Java 11ZGC 首次亮相实验性 2021年 ──→ Java 17ZGC/Shenandoah 正式 GALTS 版本 2023年 ──→ Java 21虚拟线程Project Loom正式发布 2024年 ──→ Java 23/24持续演进中从最初为嵌入式设备而生到支撑全球数十亿设备和海量后端服务JVM 完成了历史上少有的技术跨越。1.3 JVM 规范与实现不只有 HotSpot很多开发者以为 JVM 就是 HotSpot其实JVM 是一套规范由 JVM Specification 定义任何人都可以按照规范实现自己的 JVMJVM 实现开发方特点HotSpotOracle原 Sun最主流生产首选C2 编译器性能强OpenJ9Eclipse/IBM低内存占用启动快适合容器部署GraalVMOracle支持 AOT 编译多语言运行时Zing (Azul)Azul Systems超低停顿C4 GC商业版Android ARTGoogle为移动设备优化Dalvik 的继承者Jikes RVMIBM 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-3C1调用次数达阈值基础优化较快Tier 4C2超热点代码激进优化内联/逃逸分析最优工程意义这就是为什么 JVM 程序会有预热Warm-up现象——刚启动时较慢运行一段时间后性能大幅提升。在高性能场景合理配置预热策略非常重要。3.3 内存分配的快车道TLAB每次新建对象JVM 不是直接在堆上竞争式分配而是给每个线程预先在 Eden 区划分一块线程本地分配缓冲区TLABThread-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:NewRatio2# 老年代:年轻代 2:1与-Xmn二选一4.2 方法区Method Area/ 元空间Metaspace方法区存储类的元信息包括类的全限定名、父类信息、接口列表字段和方法的描述符运行时常量池字符串字面量、符号引用等静态变量方法的字节码、异常表JDK 演进关键变化JDK 1.7 及以前永久代 (PermGen) ├── 在堆内分配 ├── -XX:MaxPermSize256m 控制大小 └── 容易出现 java.lang.OutOfMemoryError: PermGen space JDK 1.8元空间 (Metaspace) ├── 迁移到本地内存Native Memory不受堆限制 ├── -XX:MaxMetaspaceSize256m 控制上限不设则默认无限 └── 大幅减少了 PermGen OOM 问题4.3 虚拟机栈VM Stack每个线程拥有独立的虚拟机栈每次方法调用都会创建一个栈帧Stack Frame线程 A 的虚拟机栈 ┌─────────────────────────────────┐ │ 栈帧methodC() │ ← 当前执行方法栈顶 │ 局部变量表 | 操作数栈 | 动态链接 │ ├─────────────────────────────────┤ │ 栈帧methodB() │ ├─────────────────────────────────┤ │ 栈帧methodA() │ ├─────────────────────────────────┤ │ 栈帧main() │ ← 栈底 └─────────────────────────────────┘栈帧的四大组成部分局部变量表存放方法参数和局部变量基本类型直接存值引用类型存对象地址操作数栈方法执行时的工作区字节码指令的操作数在此入栈出栈动态连接指向运行时常量池中本方法的符号引用方法返回地址方法退出后回到调用者的位置常见异常StackOverflowError栈深度超过-Xss限制无限递归OutOfMemoryError动态扩展栈时内存不足4.4 程序计数器PC Register程序计数器是 JVM 中最小的内存区域也是唯一不会发生 OOM 的区域。每个线程独有保存当前正在执行的字节码指令的行号偏移量执行 Native 方法时值为 UndefinedCPU 在多线程切换时依靠 PC 恢复现场五、执行引擎让字节码活起来5.1 解释器Interpreter解释器逐条读取字节码指令并执行优点是启动快缺点是执行效率低。HotSpot 中的解释器是模板解释器Template Interpreter为每条字节码指令预生成了对应的本地机器码片段比纯软件解释快很多。5.2 JIT 编译器C1 编译器Client Compiler注重启动速度优化较简单执行简单的内联、方法内联、去虚化等优化适合对延迟敏感的客户端应用C2 编译器Server Compiler注重峰值吞吐量优化激进耗时逃逸分析、标量替换、锁消除、循环展开等高级优化适合长期运行的服务端应用分层编译Tiered CompilationJDK 8 默认启用C1 和 C2 协同工作程序先用 C1 快速编译获得基础性能提升运行一段时间后超热点代码再被 C2 重新编译获得最优性能。5.3 垃圾收集器GC垃圾收集器是执行引擎的重要组成部分负责自动管理堆内存JVM 垃圾收集器演进路线图 Serial GC ──────────────────────────────────→ │ 单线程简单适合小堆 ▼ Parallel GC ─────────────────────────────────→ │ 多线程高吞吐量JDK8默认 ▼ CMS GC ──────────────────────────────────────→ │ 并发标记清除低停顿JDK9废弃 ▼ G1 GC ───────────────────────────────────────→ │ Region分代可预测停顿JDK9默认 ▼ ZGC ─────────────────────────────────────────→ │ 超低停顿10msJDK15 GA ▼ Shenandoah GC ───────────────────────────────→ RedHat研发并发整理JDK15 GA本系列将用多篇文章深入讲解各 GC 的工作原理这里先有个整体印象。六、JVM 规范与实现的边界理解 JVM 规范和具体实现的区别是避免对号入座错误认知的关键内容规范Specification要求HotSpot 实现运行时数据区划分规定各区域的逻辑概念具体内存布局由实现决定方法区逻辑概念JDK8 前PermGenJDK8Metaspace垃圾回收规范完全不要求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 21JVM 完成了从嵌入式到互联网再到云原生的三次蜕变架构全景类加载子系统 → 运行时数据区 → 执行引擎三大主干各司其职运行时数据区堆对象、方法区/元空间类信息、虚拟机栈方法调用、程序计数器指令追踪执行引擎解释器JITC1/C2的混合执行模式分层编译平衡启动速度与峰值性能GC 演进Serial → Parallel → CMS → G1 → ZGC/Shenandoah停顿时间不断缩短一句话记住 JVM 的本质JVM 是一台虚拟的计算机它有自己的指令集字节码、内存模型堆/栈/方法区和执行引擎解释JIT运行在真实操作系统之上对上屏蔽了平台差异对下利用 JIT 和 GC 极致榨取硬件性能。系列导航下一篇【JVM深度解析】第02篇类加载机制深度解析系列目录JVM深度解析参考资料《深入理解Java虚拟机第3版》— 周志明著机械工业出版社JVM SpecificationJava SE 21HotSpot JVM 架构概览OpenJDK 源码仓库Java Platform, Standard Edition HotSpot Virtual Machine GC Tuning GuideAleksey Shipilёv: JVM Anatomy Quarks