什么是类加载
Java 类加载是指将 Java 字节码文件加载到 Java 虚拟机(JVM)中,并将其转化为可以执行的可执行代码的过程。当 Java 程序在运行时引用某个类时,JVM 会首先检查是否已经加载该类,如果没有加载,则会通过类加载器加载该类。
类加载器负责加载字节码文件(.class 文件)到 JVM 内存中,并生成一个 java.lang.Class 对象,该对象包含了类的所有信息。类加载器主要完成以下三个步骤:
-
加载:查找并加载类的字节码文件。类加载器根据类的名称来查找字节码文件,然后将字节码文件加载到内存中。
-
链接:将类的字节码文件链接为可执行的代码。链接过程包括验证、准备(为静态变量分配内存并设置初始值)、解析(将字节码中的符号引用转换为直接引用)等。
-
初始化:执行类的初始化代码,包括静态变量的初始化和静态代码块的执行。
类加载器采用了双亲委派模型,即先委派给上层的父类加载器进行加载,如果父类加载器无法加载,则由当前类加载器进行加载。这个机制可以确保类的加载是有序的,并且可以避免重复加载同一个类。
双亲委派模型
在Java中,类加载器负责将字节码文件加载到内存中并创建对应的Class对象,从而使得Java程序可以使用这些类。双亲委派模型是Java类加载机制的一种设计模式,它通过层次化的类加载器结构来保证类的唯一性和安全性。
具体来说,双亲委派模型的原理如下:
- 当程序需要加载一个类时,首先会委托给最顶层的类加载器——启动类加载器(Bootstrap Class Loader)。
- 启动类加载器会检查是否能够加载该类,如果能够加载,则直接进行加载;如果不能加载,则将加载请求委托给扩展类加载器(Extension Class Loader)。
- 扩展类加载器会检查是否能够加载该类,如果能够加载,则直接进行加载;如果不能加载,则将加载请求委托给应用程序类加载器(Application Class Loader)。
- 应用程序类加载器会检查是否能够加载该类,如果能够加载,则直接进行加载;如果不能加载,则抛出ClassNotFoundException异常。
这种层次化的委派机制使得每个类加载器只负责加载自己负责的类,而不负责加载其他类。这种分工明确的加载机制可以有效地避免类的重复加载和被恶意篡改的风险。
- 启动类加载器(Bootstrap Class Loader)是最顶层的类加载器,它负责加载Java核心类库,例如java.lang包下的类。
- 扩展类加载器(Extension Class Loader)是在启动类加载器之后,负责加载Java扩展类库,例如javax包下的类。
- 应用程序类加载器(Application Class Loader)是在扩展类加载器之后,负责加载应用程序的类。、
- 加载类时的顺序是:启动类加载器 -> 扩展类加载器 -> 应用程序类加载器。
另外,双亲委派模型还有以下几个特点:
- 类加载器之间形成了父子关系,每个类加载器都有一个父加载器。子类加载器会首先委托给父加载器进行加载,只有在父加载器无法加载时,才会尝试自己加载。
- 父加载器通过委派给子加载器,实现了类加载的向上委托机制。这样可以确保类加载器在加载类时,先从上层类加载器查找,从而保证系统核心库的安全性。
- 双亲委派模型中的类加载器是优先加载父类路径下的类,这样可以避免自定义类和Java核心类库的冲突。
双亲委派模型的原理是通过层次化的类加载器结构,自底向上地查找并加载类,从而保证类的唯一性和安全性。这种设计模式在Java中被广泛应用,能够有效避免类的重复加载和被篡改的风险,提高了系统的稳定性和安全性。
Java 类加载过程
Java类加载的过程可以分为以下7个步骤:
-
加载(Load):加载是类加载过程的第一步。在这一步,Java虚拟机会根据类的全限定名(包括包路径)查找并加载类的二进制数据,并将其存放在方法区(Java 8之前)或元空间(Java 8及以后)中。类的二进制数据可以来自于多种来源,如本地文件系统、网络、ZIP压缩文件等。
-
验证(Verify):验证是加载过程的第二步。在这一步,Java虚拟机会对类的二进制数据进行合法性校验,确保其符合Java虚拟机规范。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证。
-
准备(Prepare):准备是加载过程的第三步。在这一步,Java虚拟机会为类的静态变量分配内存,并设置默认初始值。静态变量存放在方法区(Java 8之前)或元空间(Java 8及以后)中。
-
解析(Resolve):解析是加载过程的第四步。在这一步,Java虚拟机会将类、接口、字段和方法的符号引用解析为直接引用。符号引用是一种符号化的引用,直接引用是可以直接指向内存中的对象。
-
初始化(Initialize):初始化是加载过程的第五步。在这一步,Java虚拟机会对类的静态变量进行赋值和静态代码块的执行。初始化是类被首次主动使用时触发的,包括以下情况:创建类的实例、调用类的静态方法、访问类的静态变量、反射调用类的方法和字段,以及初始值为常量的静态变量(编译期常量)。
-
使用(Use):使用是加载过程的第六步。在这一步,Java虚拟机会根据需要使用类,执行相应的字节码指令。使用的过程中可以触发类的初始化。
-
卸载(Unload):卸载是加载过程的最后一步。在这一步,Java虚拟机会卸载不再使用的类。类的卸载通常是由Java虚拟机的垃圾回收器判断并触发的,判断标准包括:类的实例被全部回收、类的ClassLoader被回收、类的引用被全部清除。
下面是一个简单的示例代码,用于演示类加载过程的几个阶段:
public class MyClass {// 静态变量public static int count = 0;// 静态代码块static {System.out.println("静态代码块执行");count = 10;}// 静态方法public static void printCount() {System.out.println("count = " + count);}public static void main(String[] args) {MyClass.printCount();}
}
运行上述代码,可以看到输出结果为:
静态代码块执行
count = 10
可以看出,类的加载过程中,静态代码块在准备阶段进行赋值,然后在初始化阶段执行。
总结
-
加载:在该阶段,类的字节码文件被加载到内存中,并被存储在方法区中的运行时常量池内。
-
验证:在验证阶段,对字节码文件的合法性进行检查,以确保它满足Java虚拟机的规范要求,并且不会对虚拟机产生安全风险。
-
准备:在准备阶段,为类的静态变量分配内存,并设置默认初始值。
-
解析:在解析阶段,将常量池中的符号引用转换为直接引用。
-
初始化:在初始化阶段,对类的静态变量进行初始化,并执行静态代码块。
-
使用:在使用阶段,类被加载到内存中,并可以被其他类引用和使用。
-
卸载:在卸载阶段,当类或类的ClassLoader不再被引用时,类被从内存中卸载。
注意:类的加载过程是按需进行的,即只有在使用到该类时才会触发加载过程。