引言
在 Java 编程的世界里,Java 虚拟机(JVM)如同幕后的强大引擎,驱动着 Java 程序的运行。其中,类加载子系统是 JVM 启动并运行 Java 代码的关键第一步。深入理解类加载子系统,不仅有助于我们明白 Java 程序如何被加载到内存并执行,还能在优化程序性能、排查类加载相关问题时发挥重要作用。
类加载器的层次结构
启动类加载器(Bootstrap ClassLoader)
启动类加载器是 JVM 类加载机制的根基,由 C++ 编写。它负责加载 Java 核心类库,这些类库位于 JRE 的 lib
目录下,如 rt.jar
。像 java.lang.Object
、java.lang.String
等基础类便是由启动类加载器加载。由于它是 JVM 运行的基础,其加载的类库对整个 Java 运行环境至关重要。
扩展类加载器(Extension ClassLoader)
扩展类加载器继承自启动类加载器,主要加载 JRE/lib/ext
目录下的类库,或者由系统变量 java.ext.dirs
指定的目录中的类库。这些类库通常是对 Java 核心类库的扩展,比如一些安全相关的扩展类就由扩展类加载器加载。
应用程序类加载器(Application ClassLoader)
应用程序类加载器负责加载应用程序的类路径(CLASSPATH)下的所有类。在日常开发中,我们编写的业务代码以及所依赖的第三方库,若无特殊设置,都由应用程序类加载器加载。它是我们开发过程中最常接触的类加载器,理解其工作原理对于排查类加载问题至关重要。
自定义类加载器
在某些特定场景下,我们需要自定义类加载器。比如,实现类的加密加载,先对字节码文件进行加密,在加载时由自定义类加载器解密后再加载;或者实现类的隔离加载,不同模块使用不同的类加载器,避免类冲突。
自定义类加载器需要继承 ClassLoader
类,并重写关键方法,如 findClass
方法。在 findClass
方法中,我们可以自定义加载逻辑,例如从网络、特定文件系统位置加载类的字节码。
类加载的过程
加载(Loading)
加载是类加载的起始阶段。在此阶段,JVM 会根据类的全限定名获取对应的字节流。这字节流的来源可以是本地文件系统、网络,甚至是动态生成。获取字节流后,JVM 将其转换为方法区的运行时数据结构,并在堆中生成一个代表该类的 java.lang.Class
对象。例如,当我们编写一个简单的 HelloWorld
类,JVM 会通过加载阶段将 HelloWorld.class
文件的字节流加载到内存,并构建相关的数据结构。
验证(Verification)
- 文件格式验证:确保字节流符合 Class 文件格式规范,如文件头是否正确、魔数是否匹配、常量池是否有正确的结构等。如果字节流不符合规范,JVM 将抛出
ClassFormatError
异常。 - 元数据验证:对类的元数据信息进行语义校验,比如类是否继承了不允许继承的类(如
final
类),是否实现了接口中要求的所有方法等。 - 字节码验证:这是验证过程中最复杂的部分,确保字节码指令的语义和逻辑正确。例如,检查指令的操作数是否正确,跳转指令是否指向合法位置等,防止恶意字节码破坏 JVM 运行环境。
- 符号引用验证:确保类对其他类、方法、字段等的符号引用的正确性。在解析阶段前,类中的引用都是符号引用,验证阶段要确保这些引用在后续解析时能正确转换为直接引用。
准备(Preparation)
准备阶段为类的静态变量分配内存并设置初始值。这里的初始值是零值,例如 int
类型的静态变量初始值为 0,Object
类型的静态变量初始值为 null
。但需要注意的是,对于被 final
修饰的静态常量,在准备阶段会直接赋予其定义的值,如 public static final int VALUE = 10;
,在准备阶段 VALUE
就被赋值为 10。
解析(Resolution)
解析阶段将符号引用替换为直接引用。符号引用是在编译阶段产生的,以一组符号来描述所引用的目标,而直接引用是可以直接指向目标的指针、相对偏移量等。例如,类对其他类的引用在编译时是符号引用,在解析阶段会根据类的全限定名找到对应的类,并将引用转换为直接引用,使得 JVM 可以直接访问目标类。解析的类型包括类或接口的解析、字段解析、方法解析等。
初始化(Initialization)
初始化阶段执行类构造器 <clinit>()
方法。<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {}
)中的语句合并产生的。执行顺序按照语句在源文件中出现的顺序。
类初始化的触发条件分为主动引用和被动引用。主动引用会触发类的初始化,比如创建类的实例、访问类的静态变量(除 final
常量)、调用类的静态方法等。而被动引用不会触发初始化,例如通过子类引用父类的静态变量,只会触发父类的初始化,不会触发子类的初始化。