Skip to content

JVM 整体架构图解

想象一下这个场景:你在 IDE 里敲下 System.out.println("Hello"),然后按下运行。几毫秒后,屏幕上出现了 "Hello"。

但在这几毫秒里,JVM 内部发生了什么?

答案是:类加载器把 .class 文件加载进内存 → 字节码执行引擎有条不紊地执行指令 → 垃圾回收器在后台虎视眈眈地盯着每一个对象。

今天,我们就来揭开 JVM 架构的神秘面纱。

JVM 架构全景图

JVM 的整体架构,可以用下面这张图来概括:

┌─────────────────────────────────────────────────────────────────┐
│                         JVM 进程空间                              │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      类加载子系统                             ││
│  │  Bootstrap ClassLoader → ExtClassLoader → AppClassLoader   ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      运行时数据区                             ││
│  │                                                             ││
│  │  ┌─────────────────────┐    ┌─────────────────────────────┐ ││
│  │  │     线程私有区域      │    │      线程共享区域           │ ││
│  │  │  ┌─────────────────┐│    │  ┌───────────────────────┐ │ ││
│  │  │  │  程序计数器 (PC)  ││    │  │        堆 (Heap)      │ │ ││
│  │  │  └─────────────────┘│    │  │  ┌─────┬─────┬─────┐  │ ││
│  │  │  ┌─────────────────┐│    │  │  │Eden │Surv │Surv │  │ ││
│  │  │  │   虚拟机栈       ││    │  │  │     │ 0   │ 1   │  │ ││
│  │  │  │  (Stack)        ││    │  │  │     │     │     │  │ ││
│  │  │  └─────────────────┘│    │  │  ├─────┴─────┴─────┤  │ ││
│  │  │  ┌─────────────────┐│    │  │ │    Old Gen    │  │ ││
│  │  │  │   本地方法栈     ││    │  │ │               │  │ ││
│  │  │  │  (Native Stack)  ││    │  │ └───────────────┘  │ ││
│  │  │  └─────────────────┘│    │  └───────────────────────┘ ││
│  │  └─────────────────────┘│    │                            ││
│  │                        │    │  ┌───────────────────────┐ ││
│  │                        │    │  │     方法区 (Method    │ ││
│  │                        │    │  │     Area/JDK8+       │ ││
│  │                        │    │  │     Metaspace)       │ ││
│  │                        │    │  └───────────────────────┘ ││
│  │                        │    └─────────────────────────────┘│
│  │  ┌─────────────────────┐                                    │
│  │  │      直接内存        │                                    │
│  │  │  (Direct Memory)    │                                    │
│  │  └─────────────────────┘                                    │
│  └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      执行引擎                                ││
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  ││
│  │  │   解释器      │  │  JIT 编译器   │  │     垃圾回收器    │  ││
│  │  │ (Interpreter) │  │ (JIT Compiler)│  │   (Garbage       │  ││
│  │  │              │  │  C1 / C2      │  │    Collector)    │  ││
│  │  └──────────────┘  └──────────────┘  └──────────────────┘  ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                      本地接口 / 本地库                       ││
│  │                   JNI (Java Native Interface)                ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

这张图清晰地展示了 JVM 的四大核心组成部分。

一、类加载子系统:JVM 的「入职培训部」

类加载子系统负责.class 文件加载到 JVM 中,形成可以被执行的类对象。

它的工作流程是:

.class 文件 → 加载(Loading) → 验证(Verification) → 准备(Preparation)
           → 解析(Resolution) → 初始化(Initialization)

加载阶段,类加载器会通过类的全限定名找到对应的 .class 文件,将其字节流转化为方法区的运行时数据结构,并在堆中生成一个 Class 对象作为访问入口。

验证阶段,确保加载的类符合 JVM 规范,不会危害 JVM 安全。

准备阶段,为类的静态变量分配内存,并设置默认初始值(int 设为 0boolean 设为 false 等)。

解析阶段,将符号引用替换为直接引用。

初始化阶段,执行静态代码块和静态变量的赋值。

关于类加载器的层级关系,我们后面会有专题讲解。

二、运行时数据区:JVM 的「memory」

运行时数据区是 JVM 管理的内存区域,也是面试中的高频考点。

线程私有区域

这些区域每个线程都有自己独立的副本,线程之间互不影响:

区域作用大小
程序计数器存储当前线程执行的字节码指令地址极小(几乎可忽略)
虚拟机栈管理方法调用,存储栈帧(局部变量表、操作数栈等)-Xss 控制,默认 1MB
本地方法栈为 Native 方法服务(JNI 调用)-Xss 控制或默认

线程共享区域

这些区域被所有线程共享:

区域作用大小控制
堆(Heap)存储对象实例和数组,是 GC 的主要战场-Xms / -Xmx
方法区(JDK 7 及之前为 PermGen)存储类信息、常量、静态变量、JIT 编译后的代码JDK 7: -XX:PermSize/MaxPermSize<br>JDK 8+: -XX:MetaspaceSize/MaxMetaspaceSize

直接内存

直接内存不属于 JVM 堆,而是操作系统的本地内存。通过 ByteBuffer.allocateDirect() 分配,受 -XX:MaxDirectMemorySize 限制。

NIO 的零拷贝技术就依赖直接内存实现。

三、执行引擎:JVM 的「CPU」

执行引擎是 JVM 的核心,负责执行字节码指令

解释器 vs JIT 编译器

  • 解释器:逐行解释执行字节码,启动快,但执行效率低
  • JIT 编译器:将热点代码编译为本地机器码缓存起来,后续直接执行机器码,性能接近 native 代码

HotSpot 默认采用分层编译

  • C1 编译器:快速编译,编译激进程度低
  • C2 编译器:深度优化,编译激进程度高
字节码 → 解释执行(同时记录调用次数)
     ↓ 达到阈值(-XX:CompileThreshold,默认 10000)
   C1 即时编译
     ↓ 达到更高阈值
   C2 深度编译

垃圾回收器

GC 是执行引擎的重要组成部分,负责自动回收不再使用的对象所占用的内存。

常见的垃圾收集器包括:

  • 年轻代:SerialParNewParallel Scavenge
  • 老年代:Serial OldParallel OldCMS
  • 全堆:G1ZGCShenandoah

关于 GC 的详细内容,后面会有系列专题。

四、本地接口与本地库

JNI(Java Native Interface)提供了 Java 与本地代码(C/C++)交互的能力。

当你调用 System.loadLibrary() 加载一个 native 库时,就通过 JNI 与本地方法栈交互。很多 Java 底层库(如 java.io 的部分实现)内部就调用了 native 方法。

各区域与硬件的对应关系

从物理机器的角度看 JVM:

物理机              JVM
─────────────────────────────
CPU          →  执行引擎(解释器 + JIT)
寄存器       →  程序计数器
内存         →  堆 + 方法区
缓存         →  TLAB(线程本地分配缓冲)
磁盘         →  直接内存(NIO)

理解这个对应关系,有助于你在排查问题时准确定位瓶颈在哪里。

总结

JVM 架构的核心就是「加载—存储—执行—回收」这四个环节的循环:

  1. 类加载子系统.class 加载进内存
  2. 运行时数据区为对象和代码提供存储空间
  3. 执行引擎负责执行字节码
  4. 垃圾回收器负责回收不再使用的内存

面试中,面试官可能会从以下几个角度追问:

  • 「线程私有区域和共享区域有什么区别?」
  • 「为什么堆需要 GC,而栈不需要?」
  • 「JIT 编译和解释执行各有什么优劣?」

留给你的思考题:

如果你发现一个 Java 进程占用了 8GB 内存,但堆只设置了 4GB,这多出来的 4GB 去了哪里?

提示:直接内存、Metaspace、线程栈、本地代码映射等都可能占用内存。下篇文章我们会详细讲解运行时数据区的各个组成部分。

基于 VitePress 构建