当前位置: 首页 > news >正文

【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 实现开发方特点
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-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深度解析

参考资料

  1. 《深入理解Java虚拟机(第3版)》— 周志明著,机械工业出版社
  2. JVM Specification(Java SE 21)
  3. HotSpot JVM 架构概览
  4. OpenJDK 源码仓库
  5. Java Platform, Standard Edition HotSpot Virtual Machine GC Tuning Guide
  6. Aleksey Shipilёv: JVM Anatomy Quarks

http://www.jsqmd.com/news/653143/

相关文章:

  • 5G NR调度器:从帧结构到资源分配的实战解析
  • Cadence Virtuoso导入TSMC 65nm PDK保姆级避坑指南:从解压到仿真成功全流程
  • 2026 年两款服务器面板内存占用测试:宝塔面板和 1Panel 表现如何
  • GB/T 13123-2026 竹胶合板检测
  • 免费论文AIGC检测使用指南:原理实操全攻略
  • 扫普通链接二维码打开小程序页面参数获取
  • 开发者面试内卷:突出重围的差异化战术
  • 实战解析 | Workbench多单元混合建模在静力学分析中的高效应用
  • 当AI学会害怕和好奇——V4认知与情绪
  • 五大Web GIS地图框架深度对比:Leaflet、OpenLayers、Mapbox、Cesium与ArcGIS for JavaScript
  • 多益网络笔试里的Python哲学题怎么答?‘Explicit is better than implicit’对新手程序员意味着什么?
  • Cursor Pro激活技术深度解析:3大核心技术实现与实战指南
  • 如何用Jasminum插件3分钟搞定中文文献管理:Zotero终极效率提升指南
  • 【JVM深度解析】第02篇:类加载机制深度解析
  • DelphiZXingQRCode 实战:从零到一构建企业级二维码生成模块
  • OpenClaw Windows 一键部署全流程|解压即装+环境免配置,龙虾AI智能体本地快速落地
  • openEuler 22.03下5分钟搞定Docker安装与镜像加速(华为云镜像源实测)
  • 避开Matlab新手必踩的坑:空值判断的正确姿势(为什么a==[]永远返回false)
  • Bring up
  • 家庭网络搭建指南:从光猫到路由器的全流程解析
  • 将小龙虾接入ClawBot教程,用微信就能出电影解说视频
  • vue 拖拽排序实现方案
  • 三堵墙逼出来的智慧——V3障碍与感知
  • 2026奇点大会最重磅签约项目曝光:3省医保局联合接入AI咨询结算系统,附可立即套用的DRG-AI交叉计费对照表
  • 如何在Obsidian中实现Excel表格的无缝编辑?终极Excel插件让笔记与数据完美融合
  • 面试官最爱问的哈希表实战:用C++手撕‘存在重复元素II’和‘字母异位词分组’
  • 从空调温控到智能驾驶:模糊推理在工业控制中的实战避坑指南
  • seL4微内核入门-代码下载运行及资料
  • 用 QClaw 做了一个工程合同风险审计技能,说说我的完整实践过程
  • PLDM实战指南:加速卡层级建模与传感器配置