每个 Java 开发人员都知道字节码是由 JRE(Java Runtime Environment) 执行的。但是很少有人知道 JRE 是 Java虚拟机(JVM) 的实现,它负责分析字节码,解释代码并执行。了解 JVM 体系结构,对于作为一名开发人员来说是非常重要的。因为它能让我们能够更加高效地编写代码。在本文中,我们将更加深入了解 Java 中的 JVM体 系结构以及 JVM 的各种 组件。
什么是 JVM
虚拟机是物理机器的软件实现。Java 的开发遵循 WORA(一次编写随处运行)理念,它运行在 VM(虚拟机)上。编译器将 Java 文件编译成 Java.class 文件,之后,将 .class 文件输入到 JVM 中,加载并执行该类文件。下图是 JVM 的体系结构:
JVM 体系结构图
JVM 是如何工作的?
如上图所示,JVM 分为三个主要子系统:
类加载器子系统
运行时数据区
执行引擎
1、类加载器子系统
Java 的动态类加载功能是由类加载器子系统处理的。它负责加载、链接,并且在运行时首次引用类的时候初始化类,而不是在编译期间。
1.1、加载
这个组件负责加载类。BootStrap 类加载器、Extension 类加载器和 Application 类加载器都是实现了这个功能的三大类加载器。
BootStrap 类加载器 —— 负责从 classpath 加载类,如果类不存在,将只加载 rt.jar。这个加载器的优先级最高。
Extension 类加载器 —— 负责加载扩展文件夹(jre\lib)中的类。
Application 类加载器 —— 负责加载应用级 classpath和环境变量指向的路径下的类。
上述类加载器在加载类文件时遵循委托层次结构算法(双亲委派模型)。
1.2、链接校验 —— 字节码验证器校验生成的字节码是否正确,如果校验失败,我们将得到校验错误信息。
准备 —— 对于所有的静态变量申请内存并分配默认值。
解析 —— 所有标记的内存引用从方法区被替换成的原始引用。
1.3、初始化
这是类加载的最后阶段,所有静态变量都将被分配原值,静态代码块将被执行。
2、运行时数据区
运行时数据区被划分为五个主要部分:
方法区 —— 所有类级数据都将存储在这里,包括静态变量。每一个 JVM 只有一个方法区,并且它是一个共享资源。
堆区 —— 所有对象及其对应的实例变量和数组等存储在此,每个 JVM 同样只有一个堆区。由于方法区和堆区是多线程内存共享,因此存储的数据是非线程安全的。
栈区 —— 每个线程都会创建一个单独的运行时栈。在每次方法调用时,都会在栈内存中创建一个栈帧(Stack Frame)。所有局部变量都将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧可以被划分为三个实体:
局部变量数组 —— 与方法中有多少局部变量有关,相应的值将存储在此处。
操作数栈 —— 如果有中间操作需要被执行,操作数栈将作为运行时工作区来执行操作。
帧数据 —— 与方法相对应的所有符号存储在此。在有异常情况下,catch 块的信息被保留在帧数据中。
PC(程序计数器,Program Counter)寄存器 —— 每一个线程都有单独的PC 寄存器,一旦执行指令,PC 寄存器将被下一条指令更新,保存当前执行指令的地址。
本地方法栈 —— 本地方法栈保存本地方法相关信息,每一个线程都会创建一个单独的本地方法栈。
3、执行引擎
分配到运行时数据区的字节码将被执行引擎执行。执行引擎读取字节码并逐一执行。
解释器 —— 解释器能更加快速地解释字节码,但执行速度缓慢。解释器的缺点是当多次调用一个方法时,每次都要重新解释。
JIT 编译器 —— JIT 编译器弥补了解释器的不足。执行引擎使用解释器来转换字节码,当发现重复的代码时,它将使用 JIT 编译器来编译整个字节码并转换为本地代码。本地代码将直接被重复的方法所调用,从而提高系统性能。
中间代码生成器 —— 生成中间代码。
代码优化器 —— 负责优化上述生成的中间代码。
目标代码生成器 —— 负责生成机器码或者本地代码。
分析器 —— 一个特殊的组件,负责查找热点代码,比如一个方法是否被调用多次。
垃圾回收器 —— 回收并删除未引用的对象。可以通过调用 System.gc() 来触发垃圾回收,但不能保证它一定执行。JVM 的垃圾回收是回收被创建的对象。
Java 本地接口(JNI):JNI 与本地方法库交互,并为执行引擎提供本地方法库。
本地方法库(Native Method Libraries):它是执行引擎所需的本地库集合。
本作品采用知识共享署名 4.0 国际许可协议进行许可,转载请保留原文和译文链接及作者版本信息。