类加载机制深度解析
Java 类型从被虚拟机加载到卸载,生命周期分为:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载。其中验证、准备、解析统称为链接(Linking)。理解每个阶段做什么,是理解类加载器与线上类冲突问题的前提。
生命周期总览
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
└──────── 链接(Linking)────────┘第一阶段:加载(Loading)
加载阶段由类加载器完成三件事:
- 通过全限定名获取定义此类的二进制字节流(class 文件、jar、网络、动态生成均可)
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构(Class 对象)
- 在堆中生成该类的
java.lang.Class对象,作为访问方法区类型数据的入口
注意:数组类不通过类加载器创建,由虚拟机直接在内存中构造,其元素类型仍由类加载器加载。
第二阶段:链接(Linking)
验证(Verification)
确保字节流符合虚拟机规范且不会危害虚拟机自身安全,包括文件格式验证、元数据验证、字节码验证、符号引用验证。字节码验证是核心,通过数据流分析确认类型安全。
准备(Preparation)
为类变量(static 字段)分配内存并设置零值(如 int 为 0,引用为 null)。注意此时尚未执行任何 Java 代码:
public static int value = 123;准备阶段后 value = 0,赋值为 123 发生在初始化阶段。但 static final 常量(编译期常量)会在准备阶段直接赋初值:
public static final int CONST = 123; // 准备阶段即为 123解析(Resolution)
将常量池中的符号引用替换为直接引用。涉及类/接口、字段、方法、方法类型等解析。解析时机上,虚拟机规范允许延迟到第一次使用时才解析(惰性解析),HotSpot 默认如此。
第三阶段:初始化(Initialization)
初始化阶段执行 <clinit>() 方法,即类构造器:
- 收集所有类变量的赋值动作与静态代码块,按源码顺序合成
<clinit>() <clinit>()由虚拟机保证线程安全(同一时刻只允许一个线程初始化该 class)- 主动使用才会触发初始化:
触发初始化的 6 种主动使用:
1. new 实例、读取/设置静态字段(非常量)、调用静态方法
2. 反射调用(Class.forName 等)
3. 初始化子类时先初始化父类
4. JVM 启动时初始化主类(含 main 方法的类)
5. JDK 7+ 动态语言支持(MethodHandle 等)
6. 接口默认方法不触发初始化的被动使用:通过子类引用父类静态字段、定义数组引用、引用编译期常量。
双亲委派模型
自 JDK 1.2 起的默认类加载模型。除启动类加载器外,其余类加载器都有父加载器:
启动类加载器 Bootstrap(<JAVA_HOME>/lib,C++ 实现)
↑
扩展类加载器 Platform/Extension(<JAVA_HOME>/lib/ext,JDK 9 起为 Platform)
↑
应用类加载器 Application(classpath 下的类)工作流程:一个类加载器收到加载请求,先委派给父加载器尝试加载,父加载器加载不到,才由自己加载。
带来的好处
- 避免重复加载:同一个类在各级加载器中只会被加载一次
- 核心类安全:java.lang.String 永远由 Bootstrap 加载,防止自定义同名类替换 JDK 核心类
打破双亲委派的三种方式
方式一:重写 loadClass,自定义类加载器
双亲委派逻辑在 loadClass() 中,重写它即可改变加载顺序:
public class BreakParentFirst extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 先自己加载,再委派给父加载器 —— 破坏双亲委派
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
c = findClass(name); // 自己先尝试
} catch (ClassNotFoundException e) {
c = super.loadClass(name, resolve); // 再交父类
}
}
if (resolve) resolveClass(c);
return c;
}
}
}Tomcat 的 WebAppClassLoader 即此思路:每个 Web 应用独立加载自己的类,应用之间互不干扰,还能优先加载应用自身的类。
方式二:线程上下文类加载器(SPI)
双亲委派模型下,Bootstrap 加载的核心类无法反向委托应用类。JDBC 等 SPI 机制需要由核心类(DriverManager)加载第三方实现类,于是引入线程上下文类加载器打破"自底向上"的限制:
Thread.currentThread().setContextClassLoader(loader);JNDI、JDBC、JAXB 等均依赖此机制:核心类用线程上下文类加载器加载 SPI 实现。
方式三:模块化与热部署框架
- OSGi:每个 Bundle 一个类加载器,按 Bundle 依赖关系查找类,实现模块化隔离与动态装卸
- 热部署框架(JRebel、Spring Boot DevTools):通过自定义类加载器 + 每次变更重建加载器,让新类替换旧类
- Arthas 等诊断工具:绕过应用类加载器,直接向目标类加载器注入增强逻辑
常见问题
- ClassNotFoundException:类加载器找不到目标类,多为依赖缺失、classpath 配置错误
- NoClassDefFoundError:类编译存在但运行期初始化失败(如静态块抛异常),或依赖版本不一致
- 类冲突(版本问题):同一全限定名被多个类加载器加载,可结合
-verbose:class查看类来自哪个 jar - ClassCastException 诡异场景:两个类加载器各加载一份同名类,强转必然失败