架构师成长(三)之深入剖析类加载过程原理

一、引言

在 Java 程序的运行过程中,类加载是一个至关重要的环节。它负责将类的字节码文件加载到 Java 虚拟机(JVM)中,并进行一系列的处理,使得类能够被程序正常使用。在 JDK 1.8 及以后的版本中,JVM 的内存结构发生了一些变化,如永久代被元空间取代,这也对类加载的过程产生了一定的影响。下面将详细解析类加载的具体过程以及在 JVM 相应区域所执行的操作。

架构师成长(三)之深入剖析类加载过程原理_第1张图片

二、类加载的生命周期概述

类从被加载到 JVM 中开始,到卸载出内存为止,其生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中,加载、验证、准备、解析和初始化属于类加载过程。

三、类加载器与类加载过程各阶段在 JVM 中的详细情况

(一)加载(Loading)

  1. 从外部获取字节流:类加载器负责从文件系统、网络或其他数据源获取类的字节码文件(.class 文件)的字节流。例如,应用程序类加载器会在用户类路径(classpath)中查找 .class 文件并读取其字节流。这个过程并不在 JVM 的特定内存区域进行,而是通过类加载器与外部存储进行交互。
  2. 转换为方法区的运行时数据结构:将获取到的字节流所代表的静态存储结构转换为方法区(JDK 1.8 及以后为元空间)的运行时数据结构。元空间是直接内存的一部分,它存储类的元数据信息,如类的版本、字段、方法、接口等描述信息,以及常量池、静态变量的元数据等。这些元数据信息为后续类的使用提供了基础。
  3. 生成 Class 对象:在堆内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中该类的各种数据的访问入口。程序可以通过这个 Class 对象来访问类的各种信息,如调用类的方法、获取类的字段等。例如,通过 Class.forName("com.example.MyClass") 可以获取到 MyClass 类的 Class 对象,进而对该类进行操作。

(二)验证(Verification)

验证阶段主要在方法区(元空间)对加载进来的类的字节流进行检查。此阶段确保字节流所包含的信息符合 Class 文件格式规范,并且不会危害 JVM 的安全。具体包括以下几个方面的验证:

  1. 文件格式验证:检查字节流是否以 0xCAFEBABE 开头,主次版本号是否在当前 JVM 支持的范围内,常量池中的常量类型是否正确等。这些检查主要是针对字节流的二进制格式进行的,确保其符合 Class 文件的基本规范。
  2. 元数据验证:对类的元数据信息进行语义分析,确保其符合 Java 语言规范。例如,验证类是否有父类(除了 java.lang.Object),类中的字段和方法是否有合法的签名,是否存在循环继承等问题。
  3. 字节码验证:通过对字节码指令序列的分析,确保程序语义是合法、符合逻辑的。例如,验证操作数栈和局部变量表的使用是否正确,跳转指令是否会跳转到合法的位置,方法调用是否符合访问权限等。
  4. 符号引用验证:在解析阶段将符号引用转换为直接引用时,对符号引用进行验证。例如,验证符号引用所引用的类、字段和方法是否存在,访问权限是否合法等。

(三)准备(Preparation)

准备阶段涉及元空间和堆两个区域。在元空间存储静态变量的元数据信息,而在堆中为类的静态变量(被 static 修饰的变量)分配内存,并设置默认初始值。这个阶段并不执行静态变量的赋值操作,只是根据变量的类型赋予默认值。例如:

  1. 对于 int 类型的静态变量,默认初始值为 0。
  2. 对于 boolean 类型的静态变量,默认初始值为 false
  3. 对于引用类型的静态变量,默认初始值为 null

但是,如果静态变量被 static final 修饰,在准备阶段会直接将其初始化为指定的值。例如:

public class PrepareExample {
    public static int staticVar; // 准备阶段在堆中分配内存,初始值为 0,元数据存于元空间
    public static final int CONSTANT = 10; // 准备阶段在堆中分配内存并初始化为 10,元数据存于元空间
}

(四)解析(Resolution)

解析阶段同样主要在方法区(元空间)进行,其主要任务是将常量池中的符号引用替换为直接引用。符号引用是一种用符号来描述所引用的目标,如类的全限定名、字段和方法的名称及描述符等;直接引用是直接指向目标的指针、相对偏移量或句柄等。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等 7 类符号引用进行。例如,当一个类引用了另一个类的方法时,在解析阶段会将该方法的符号引用转换为实际的内存地址,以便在运行时能够直接调用该方法。

(五)初始化(Initialization)

初始化阶段会执行类的静态代码块和静态变量的显式赋值语句。这些代码会按照它们在类中出现的顺序依次执行。此阶段的操作涉及到元空间中的类的元数据信息以及堆中的静态变量实例。具体来说:

静态代码块执行:静态代码块中的代码会在类初始化时执行一次,用于进行一些静态资源的初始化操作。例如:

public class InitializationExample {
    public static int staticVar;
    static {
        staticVar = 20; // 静态代码块,在类初始化时执行,修改堆中 staticVar 的值
    }
}

静态变量显式赋值:静态变量的显式赋值语句也会在初始化阶段执行。例如:

public class InitializationExample2 {
    public static int staticVar = 30; // 静态变量显式赋值,在类初始化时执行,修改堆中 staticVar 的值
}

四、总结

在 JDK 1.8 及以后的版本中,类加载的各个阶段在 JVM 的不同区域协同完成。加载阶段从外部获取字节流并在元空间和堆中构建类的运行时数据结构;验证阶段在元空间对字节流进行安全性和规范性检查;准备阶段在元空间存储静态变量元数据,在堆中为静态变量分配内存并设置默认值;解析阶段在元空间将符号引用转换为直接引用;初始化阶段执行类的静态代码块和静态变量的显式赋值操作,涉及元空间和堆中的相关数据。这些阶段共同确保了类能够正确加载和初始化,为 Java 程序的运行提供了基础。深入理解类加载的具体过程原理,有助于开发者更好地掌握 Java 程序的运行机制,解决开发过程中遇到的类加载相关问题。

你可能感兴趣的:(java技术架构师成长专栏,jvm,java,架构师,java底层原理)