Skip to content

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 对象进入 JVM

Java 的类加载器是分层的:引导类加载器(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 和本地代码之间的桥梁:

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 到底经历了什么。

基于 VitePress 构建