一、序言
本文和大家聊聊 JVM 的基本概念和类加载子系统。
二、什么是 JVM
JVM(Java 虚拟机)是一种在计算机上运行 Java 字节码的虚拟机。它是 Java 语言的核心组件之一,允许 Java 程序在不同的硬件平台上执行,实现了“一次编写,到处运行” 的理念。
JVM 的设计理念是提供一个虚拟的执行环境,使得 Java 程序具有跨平台的特性,不受具体硬件和操作系统的限制。通过将 Java 源代码编译成字节码,再由 JVM 解释执行或编译成机器码执行,Java 程序可以在不同的平台上运行,而不需要重新编写和编译。
三、JVM 体系结构
JVM 主要有以下几个重要组成部分:
- 类加载器子系统(Class Loader Subsystem):负责加载类文件到 JVM 中,并生成对应的类对象。
- 运行时数据区域(Runtime Data Area):包括方法区、堆、栈、程序计数器和本地方法栈等内存区域,用于存储程序执行过程中的数据。
- 执行引擎(Execution Engine):负责执行编译后的 Java 字节码,将其转换为机器码并执行。
- 本地方法接口(Native Method Interface):允许 Java 程序调用本地方法,与底层的操作系统和硬件进行交互
- 本地方法库(Native Method Libraries):包含一组本地方法的库,供 JVM 调用。
四、类加载子系统
类加载子系统是 Java 虚拟机(JVM)的一个重要组成部分,负责加载类文件并将其转换为 JVM 中的可用结构。它主要有以下几个主要功能:
- 加载(Loading):类加载器负责将类文件加载到 JVM 中。这个过程包括查找并加载类文件,通常从文件系统或网络中加载。
- 链接(Linking):链接阶段包括三个步骤:验证(Verification)、准备(Preparation)和解析(Resolution)。在验证阶段,类加载器确保类文件的字节码符合 JVM 规范,并且不会有安全漏洞。在准备阶段,静态变量分配内存并赋予默认值。在解析阶段,将符号引用转换为直接引用。
- 初始化(Initialization):在初始化阶段,类加载器执行类的静态初始化器和静态变量赋值。这是类加载过程中的最后一步,也是类加载的重要部分。
4.1 加载阶段
类加载子系统的加载阶段是 JVM 类加载过程的第一阶段。在这个阶段,JVM 会做以下几件事:
- 获取二进制字节流:通过一个类的全限定名(全限定名就是类名全称,带包路径的用点隔开,例如:
java.lang.String
)获取定义此类的二进制字节流。 - 转化为运行时数据结构:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 生成 java.lang.Class 对象:在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
加载阶段完成后,JVM 会进入链接阶段,包括验证、准备和解析三个子阶段。
4.2 链接阶段
类加载子系统的链接阶段是 JVM 类加载过程的第二阶段。在这个阶段,JVM 会做以下几件事:
- 验证(Verification):验证是链接操作的第一步,它的目的是保证加载的字节码是合法、合理并符合规范。验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查:
- 格式验证:是否以魔数
OxCAFEBABE
开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。 - 语义检查:例如,是否所有的类都有父类的存在(在 Java 里,除了 Object 外,其他类都应该有父类);是否一些被定义为 final 的方法或者类被重写或继承;非抽象类是否实现了所有抽象方法或者接口方法;是否存在不兼容的方法等。
- 字节码验证:例如,在字节码的执行过程中,是否会跳转到一条不存在的指令;函数的调用是否传递了正确类型的参数;变量的赋值是不是给了正确的数据类型等。
- 符号引用验证:例如,如果一个需要使用的类无法在系统中找到,则会抛出
NoClassDefFoundError
,如果一个方法无法被找到,则会抛出NoSuchMethodError
。
- 格式验证:是否以魔数
- 准备(Preparation):为类变量分配内存并且设置该类变量的默认初始值(即赋初始零值)。这里不包含基本数据类型的字段用
static final
修饰的情况,因为final
在编译的时候就会分配了,准备阶段会显式赋值。 - 解析(Resolution):将常量池内的符号引用转换为直接引用。所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。
链接阶段完成后,JVM 会进入初始化阶段。
4.3 初始化阶段
类加载子系统的初始化阶段是 JVM 类加载过程的最后一阶段。在这个阶段,JVM 会做以下几件事:
- 执行类构造器方法 ():初始化阶段就是执行类构造器方法
<clinit>()
的过程。这个方法不同于类的构造器(是虚拟机视角下的<init>()
)。 - 为类变量赋值:通过准备阶段,类变量已经赋过一次系统要求的初始零值,而初始化阶段就是在给类变量进行赋值操作。
初始化阶段完成后,类加载过程就结束了,我们就可以使用这个类来创建对象和调用方法了。
五、FAQ
5.1 符号引用和直接引用
假设我们有以下代码:
public class Test {
public static void main(String[] args) {
SymbolicReference symbolicReference = new SymbolicReference();
symbolicReference.helloWorld();
}
}
class SymbolicReference {
public void helloWorld() {
System.out.println("Hello World");
}
}
上述代码中,Test 类引用了 SymbolicReference#helloWorld()
方法。在编译 Test 类时,编译器并不知道SymbolicReference#helloWorld()
方法在内存中的实际地址,因此只能使用符号引用来代替(即 symbolicReference.helloWorld())。符号引用是一种抽象的引用方式,它通过一组符号(如类名、方法名和描述符)来表示要引用的目标。
在类加载的解析阶段,JVM 会将这些符号引用转换为直接引用。直接引用是一种具体的引用方式,它直接指向内存中的对象或方法的地址。在这个示例中,直接引用就是 SymbolicReference#helloWorld()
方法的实际内存地址。
5.2 初始化阶段的执行顺序
在 JVM 的类加载过程中,初始化阶段通常是在解析阶段之后进行的。然而,在某些情况下,解析阶段可能会在初始化阶段之后进行。这主要取决于 JVM 的具体实现和运行时的动态链接需求。
以下情况可能会导致初始化阶段在解析阶段之前:
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()
方法的那个类),虚拟机会先初始化这个主类。 - 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 使用
java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
虽然解析阶段通常在初始化阶段之前,但在某些情况下,解析阶段可能会在初始化阶段之后进行。