JVM(Java Virtual Machine,Java 虚拟机)是 Java 程序的运行环境。它最大的贡献是实现了 “一次编写,到处运行”(跨平台),因为 JVM 屏蔽了底层操作系统的差异,Java 代码被编译成字节码(.class 文件),由不同平台上的 JVM 解释执行。

JVM 的基本原理非常庞大,但核心可以归纳为三大模块:类加载机制运行时数据区(内存模型)垃圾回收机制

以下是 JVM 基本原理的全面解析:


一、 JVM 整体架构

JVM 的生命周期和核心组件可以抽象为以下四个部分:

  1. 类加载器(ClassLoader):负责将 .class 文件加载到内存中,并生成代表该类的 java.lang.Class 对象。
  2. 运行时数据区(Runtime Data Areas):JVM 的内存模型,程序运行时的数据存放地。
  3. 执行引擎(Execution Engine):负责执行字节码。包含解释器(逐行解释执行)和 JIT 编译器(将热点代码编译成本地机器码以提高效率)。
  4. 本地方法接口(Native Interface):用于调用底层 C/C++ 编写的本地方法(Native 方法)。

二、 运行时数据区(内存模型)—— 核心重点

JVM 在运行 Java 程序时,会将内存划分为不同的区域。根据线程共享的关系,可以分为“线程私有”和“线程共享”两大类。

1. 线程私有区域(随线程生灭,不需要 GC)
  • 程序计数器(Program Counter Register)
    • 作用:记录当前线程所执行的字节码的行号指示器。如果执行的是 Java 方法,记录的是正在执行的虚拟机字节码指令地址;如果是 Native 方法,则为空。
    • 特点:唯一不会发生 OutOfMemoryError 的区域。
  • 虚拟机栈(VM Stack)
    • 作用:描述 Java 方法执行的内存模型。每个方法被执行时,都会创建一个“栈帧(Stack Frame)”用于存储局部变量表、操作数栈、动态链接、方法出口地址等信息。方法执行完毕,栈帧出栈。
    • 异常:如果线程请求的栈深度大于虚拟机允许的深度,抛出 StackOverflowError;如果允许动态扩展但无法申请到足够内存,抛出 OutOfMemoryError
  • 本地方法栈(Native Method Stack)
    • 作用:与虚拟机栈类似,只不过它是为 JVM 调用 Native 方法(如 C/C++ 代码)服务的。HotSpot 虚拟机直接将其与虚拟机栈合二为一。
2. 线程共享区域(随虚拟机生灭,GC 的主战场)
  • 堆(Heap)
    • 作用:JVM 中最大的一块内存区域,所有对象实例和数组都在这里分配内存。
    • 分代模型:为了提高 GC 效率,堆通常被分为新生代(Young Generation)老年代(Old Generation)
      • 新生代:朝生夕死,存活率低。细分为 Eden 区、Survivor 0 区、Survivor 1 区(比例默认 8:1:1)。
      • 老年代:存放长期存活的对象(如经过多次 Minor GC 依然存活的对象,或大对象)。
  • 方法区(Method Area)
    • 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • JDK 8 的变化(重要):在 JDK 8 之前,方法区被称为 “永久代(PermGen)”,使用的是 JVM 自己的内存;JDK 8 开始,取消了永久代,改为“元空间(Metaspace)”,使用的是操作系统的本地内存(Native Memory),大大减少了 OOM 的发生。
  • 直接内存(Direct Memory)
    • 作用:不属于 JVM 运行时数据区,但频繁使用。NIO(New IO)允许直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在堆中的 DirectByteBuffer 对象作为引用进行操作。避免了在 Java 堆和 Native 堆之间来回复制数据,提高了 I/O 性能。

三、 类加载机制

类加载是指把 .class 文件中的二进制数据读入内存,将其放入方法区中,并在堆区创建一个代表该类的 java.lang.Class 对象。

1. 类加载的 5 个过程
  1. 加载(Loading):通过类的全限定名获取定义此类的二进制字节流,转化为方法区的运行时数据结构,生成 Class 对象。
  2. 验证(Verification):确保 Class 文件的字节流包含的信息符合当前虚拟机的要求,保证安全性(如文件格式验证、元数据验证、字节码验证)。
  3. 准备(Preparation):为类的静态变量分配内存并设置初始值(如 int 初始值为 0,而不是代码中赋的值)。
  4. 解析(Resolution):将常量池内的符号引用(名字)替换为直接引用(内存地址/指针)。
  5. 初始化(Initialization):执行类构造器 <clinit> 方法的过程,真正执行代码中的静态变量赋值和静态代码块。这是类加载的最后一步。
2. 双亲委派模型(Parent Delegation Model)
  • 概念:类加载器之间具有层次关系(Bootstrap -> Extension -> Application -> Custom)。当一个类加载器收到类加载请求时,它自己不会先去加载,而是将请求委派给父类加载器,层层向上,直到最顶层的 Bootstrap 类加载器。只有当父加载器无法加载时,子加载器才会尝试自己加载。
  • 作用
    1. 避免类的重复加载
    2. 保护程序安全:防止用户自己编写一个 java.lang.Object 类来替换系统的核心 API(沙箱安全机制)。

四、 垃圾回收(Garbage Collection, GC)

GC 主要关注三件事:哪些内存需要回收?什么时候回收?如何回收?(主要针对堆和方法区)。

1. 如何判断对象已死?
  • 引用计数法:给对象添加引用计数器,被引用时 +1,引用失效时 -1。为 0 就是垃圾。(缺点:无法解决循环引用问题,Java 不使用)。
  • 可达性分析算法(Java 采用):通过一系列称为 “GC Roots” 的对象作为起始点,向下搜索引用链。如果一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的,可以被回收。
    • 常见的 GC Roots:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象。
2. 垃圾回收算法
  • 标记-清除(Mark-Sweep):先标记出所有需要回收的对象,然后统一清除。缺点:产生大量内存碎片。
  • 复制算法(Copying):将内存分为两半,每次只使用其中一半。GC 时,将存活对象复制到另一半,然后清空当前使用的一半。优点:无碎片,高效。缺点:浪费一半内存。(适用于新生代,因为 98% 的对象朝生夕死)。
  • 标记-整理(Mark-Compact):先标记,然后让所有存活对象向一端移动,直接清理掉边界外的内存。优点:无碎片。(适用于老年代)。
  • 分代收集算法:当前商业虚拟机的 GC 都采用这种思想。根据对象存活周期的不同,将堆分为新生代和老年代,分别采用复制算法和标记-整理/清除算法。
3. 常见垃圾收集器
  • Serial / Serial Old:单线程收集器,简单高效,适用于客户端模式。
  • ParNew:Serial 的多线程版本,常与 CMS 配合使用。
  • Parallel Scavenge / Parallel Old:多线程,关注吞吐量(运行用户代码的时间 / 总时间),JDK 8 默认。
  • CMS(Concurrent Mark Sweep):以获取最短回收停顿时间为目标,基于标记-清除算法,运行在老年代。缺点:对 CPU 敏感、会产生碎片、浮动垃圾。
  • G1(Garbage-First):面向全堆,将堆划分为多个大小相等的独立区域(Region),不再物理分代。可预测的停顿时间模型,JDK 9 之后的默认收集器。
  • ZGC / Shenandoah:新一代超低延迟收集器(停顿时间通常在 10ms 以内),采用染色指针和读屏障技术,支持 TB 级别的堆。

五、 执行引擎与 JIT 编译

字节码是跨平台的,但机器只能执行机器码。执行引擎负责将字节码转换为机器码。

  1. 解释器(Interpreter):逐行将字节码解释为机器码执行。启动快,但执行慢。
  2. JIT 编译器(Just-In-Time Compiler):在运行时,将热点代码(频繁执行的代码)编译成本地机器码并缓存起来。执行快,但编译需要时间。
  3. 代码优化技术:JIT 在编译时会进行优化,如:
    • 方法内联:将方法体直接嵌入到调用处,减少方法调用栈的开销。
    • 逃逸分析:分析对象的作用域,判断对象是否“逃逸”出方法或线程。如果没有逃逸,可以直接在栈上分配内存(方法结束自动销毁,减轻 GC 压力),或者进行锁消除

六、 总结与面试建议

如果你要准备 JVM 相关的面试,建议按照以下优先级掌握:

  1. 必考(基础):JVM 内存模型(堆、栈、方法区/元空间的区别,哪些是线程私有/共享)、GC Roots、双亲委派模型。
  2. 进阶(原理):类的加载过程、垃圾回收算法、分代收集思想、如何排查 OOM 和 CPU 飙高问题(jstat, jmap, jstack, Arthas 等工具)。
  3. 高阶(调优与前沿):G1 和 ZGC 的核心原理与区别、JVM 参数调优实战、逃逸分析等 JIT 优化技术。

理解 JVM 原理,不仅能帮你应对面试,更重要的是能让你写出更省内存、执行更快、更少 Bug 的高质量 Java 代码。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐