Java类加载过程
Java类加载机制(Class Loading)
类加载是JVM将.class
文件加载到内存,并转换成Class
对象的过程。整个过程分为加载、连接(验证、准备、解析)、初始化三个阶段。
一、三个阶段的具体分工
1. 加载阶段(Loading)
- 执行者:类加载器(ClassLoader)
- 任务:
- 根据类的全限定名(如
java.lang.String
)查找.class
文件的二进制数据(本地文件、网络等)加载到JVM内存。 - JVM将二进制数据读入内存,并在堆中生成一个
Class
对象(后续反射的入口)。
- 根据类的全限定名(如
- 关键点:
- 类加载器通过
findClass()
或defineClass()
方法完成加载。 - 双亲委派模型在此阶段生效(优先委派父加载器加载)。
- 类加载器通过
2. 连接阶段(Linking)
- 执行者:JVM(而非类加载器)
- 子阶段:
- 验证(Verification):检查字节码是否符合JVM规范(如魔数、语法校验)。
- 准备(Preparation):为静态变量分配内存并赋默认值(如
int
默认为0)。 - 解析(Resolution):将符号引用(如类名、方法名)转为直接引用(内存地址)。
- 关键点:
- 连接阶段由JVM内部实现,类加载器不直接参与。
- 解析阶段可能触发其他类的加载(如类A引用了类B,需先加载类B)。
3. 初始化阶段(Initialization)
- 执行者:JVM(调用类加载器加载的代码)
- 任务:
- 执行静态代码块(
static{}
)和静态变量的显式赋值。 - 初始化父类(如果尚未初始化)。
- 执行静态代码块(
- 关键点:
- JVM通过执行
<clinit>
方法(编译器生成的类初始化方法)完成初始化。 - 类加载器仅负责加载类的字节码,不控制初始化逻辑。
- JVM通过执行
二、类加载器(ClassLoader)
1. 分类
加载器 | 加载路径 | 备注 |
---|---|---|
Bootstrap ClassLoader | JRE/lib/rt.jar 等核心库 | C++实现,父加载器为null |
Extension ClassLoader | JRE/lib/ext 目录 | 加载扩展类 |
Application ClassLoader | 用户程序的classpath (如./target/classes ) | 默认的类加载器 |
自定义ClassLoader | 用户指定路径 | 需继承ClassLoader |
2. 双亲委派模型(Parents Delegation)
- 规则:类加载请求优先委派给父加载器处理,父加载器无法完成时才自己加载。
- 目的:避免重复加载,保证核心类(如
java.lang.Object
)的安全性和唯一性。
// 双亲委派的代码实现(ClassLoader.loadClass())
protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 检查是否已加载Class<?> c = findLoadedClass(name);if (c == null) {// 2. 委派给父加载器try {if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}// 3. 父加载器失败后自行加载if (c == null) {c = findClass(name);}}return c;}
}
三、破坏双亲委派的场景
- SPI(Service Provider Interface)
- 如JDBC的
DriverManager
需加载不同厂商的实现类(通过Thread.currentThread().getContextClassLoader()
)。
- 如JDBC的
- 热部署(如Tomcat)
- 每个Web应用使用独立的
ClassLoader
,优先加载自身路径的类。
- 每个Web应用使用独立的
四、类加载的面试问题
1. 类加载的触发条件?
- 主动使用类时(如
new
、静态方法调用、反射等),但被动引用(如子类调用父类静态变量)不会触发子类初始化。
2. 静态代码块、构造代码块、构造方法的执行顺序?
public class Example {static { System.out.println("静态代码块"); } // 类初始化时执行{ System.out.println("构造代码块"); } // 每次new对象时执行(在构造方法前)public Example() { System.out.println("构造方法"); }
}
输出:
静态代码块
构造代码块
构造方法
3. 如何自定义ClassLoader?
- 继承
ClassLoader
,重写findClass()
方法(通常不破坏双亲委派)。
五、总结
- 类加载流程:加载 → 验证 → 准备 → 解析 → 初始化。
- 双亲委派:保证核心类安全,但可被SPI、热部署等场景打破。
- 应用场景:热加载、模块化、代码加密(如自定义ClassLoader解密字节码)。