类加载机制

核心概念

Java 类加载器负责将 .class 字节码加载到 JVM 中并转化为 Class 对象,整个过程分为「加载 → 链接(验证+准备+解析)→ 初始化」,并采用「双亲委派」模型来保证类唯一性、安全性与高效性。

双亲委派模型: Java 中的类加载器形成一棵树。每个类加载器在加载类时,不是自己先找,而是先把请求交给父加载器;只有当所有父加载器都表示“我找不到”时,它才自己去从磁盘/网络等位置查找并加载字节码。这个“层层往上托关系”的流程,就叫双亲委派模型。 这样可以保证:

  • JDK 核心类只能由顶层的 BootstrapClassLoader 加载,防止用户篡改;
  • 同一个类(同一 ClassLoader + 全限定名)在 JVM 中只有一个版本,保证类型唯一;
  • 类一旦被父加载器加载过,子加载器就不会重复加载,提升性能。

🎯 核心判定规则:两个类相等的充要条件 = 同一个类加载器 + 完全相同的全限定名


一、为什么需要类加载机制与双亲委派?

1.1 问题背景(没有双亲委派会怎样)

  • 唯一性破坏:同一个类被不同类加载器加载会产生多个类型,instanceof 和类型转换失效
  • 安全隐患:用户可以随意编写 java.lang.String 并被加载,篡改核心类库
  • 性能浪费:每次都从磁盘读取字节码、重复验证、重复加载

1.2 设计动机

  • 安全性:保证 JVM 核心类库安全(java.* 包只能由 Bootstrap 加载)
  • 避免重复:同一个类只加载一次(缓存机制)
  • 职责分离:实现类加载的优先级与分工协作

二、类加载全过程详解

2.1 生命周期七阶段

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

└────────链接────────┘

1️⃣ 加载(Loading)

  • 通过全限定名查找字节码文件(来源:文件系统/jar/网络/动态生成)
  • 将字节码读入方法区(JDK8 后是元空间 Metaspace)
  • 堆中生成对应的 java.lang.Class 对象作为方法区数据的访问入口

2️⃣ 链接(Linking)

阶段 作用 典型错误
验证 检查字节码安全性、魔数 0xCAFEBABE、版本兼容性 VerifyError
准备 静态变量分配内存,赋默认值int→0, Object→null) ⚠️ final static 常量直接赋初始值 -
解析 将符号引用转为直接引用(内存地址) 可选阶段,支持动态绑定时延迟解析 NoSuchMethodError

关键区别

public static int value = 123;       // 准备阶段:value=0,初始化阶段:value=123

public static final int VALUE = 123; // 准备阶段:VALUE=123(编译期常量)

3️⃣ 初始化(Initialization)

执行 <clinit>() 方法(静态变量赋值 + static 块,按代码顺序执行)

触发初始化的 6 种时机(主动引用):

  1. new 创建对象
  2. 访问/修改静态变量(被 final 修饰的常量除外)
  3. 调用静态方法
  4. 反射调用 Class.forName("com.test.User")
  5. 初始化子类会先触发父类初始化
  6. 启动类(包含 main 方法的类)

不会触发初始化(被动引用):

// ❌ 通过子类引用父类静态字段
System.out.println(Child.parentValue);

// ❌ 通过数组定义引用类
Parent[] arr = new Parent[10];

// ❌ 引用常量(编译期常量折叠)
System.out.println(Config.CONSTANT);

2.2 完整工作流程图

请求加载类:com.example.User


1. findLoadedClass() ← 检查是否已加载(缓存)

    ↓ 未加载

2. 【双亲委派】parent.loadClass() ← 委托给父加载器


Bootstrap → Extension → Application → Custom

    ↓ 逐层向上委托

    ↓ 父加载器无法加载(ClassNotFoundException)

3. findClass() ← 当前加载器自己加载


4. defineClass() ← 将字节码转为 Class 对象


5. 链接:验证 → 准备 → 解析


6. 初始化:执行 <clinit>()


返回 Class 对象

三、双亲委派模型详解

3.1 三层默认类加载器(JDK 8)

加载器名称 加载路径 父加载器 可被程序获取 Java 实现
Bootstrap ClassLoader $JAVA_HOME/jre/lib/rt.jar 核心类库(String、Object) ❌ 返回 null C++ 实现
Extension ClassLoader $JAVA_HOME/jre/lib/ext/*.jar Bootstrap sun.misc.Launcher$ExtClassLoader
Application ClassLoader classpath 指定路径 Extension ClassLoader.getSystemClassLoader() sun.misc.Launcher$AppClassLoader
Custom ClassLoader 自定义路径 Application 用户自定义

JDK 9+ 变化

  • 模块化后,Extension 被 Platform ClassLoader 取代
  • 加载路径改为模块路径(--module-path

3.2 源码分析(java.lang.ClassLoader.loadClass

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 【步骤1】先检查是否已经加载过(缓存机制)
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 【步骤2】双亲委派:先让父加载器加载
                    c = parent.loadClass(name, false);
                } else {
                    // 【步骤3】parent=null 说明到顶层,交给 Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败,不处理异常
            }

            if (c == null) {
                // 【步骤4】父加载器都无法加载,自己尝试加载
                c = findClass(name);  // ⭐ 自定义加载器重写此方法
            }
        }
        // resolve=true 时进行链接中的解析阶段
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

关键方法对比

方法 作用 是否遵循双亲委派 自定义时是否重写
loadClass() 完整加载流程(包含双亲委派逻辑) ❌ 不推荐
findClass() 实际查找字节码的逻辑 - 推荐重写
defineClass() 将字节数组转为 Class 对象 - ❌ final 方法

3.3 典型案例

// 案例1:核心类由 Bootstrap 加载
Class<?> c1 = String.class;
System.out.println(c1.getClassLoader());  // null(代表 Bootstrap)

// 案例2:自定义 java.lang.String 会加载失败
// java.lang.SecurityException: Prohibited package name: java.lang
public class java.lang.String { }  // ⚠️ 包名保护机制

// 案例3:验证类加载器层级关系
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
ClassLoader extLoader = appLoader.getParent();
ClassLoader bootLoader = extLoader.getParent();
System.out.println("App: " + appLoader);     // sun.misc.Launcher$AppClassLoader
System.out.println("Ext: " + extLoader);     // sun.misc.Launcher$ExtClassLoader
System.out.println("Boot: " + bootLoader);   // null

// 案例4:打破双亲委派(SPI 机制)
DriverManager.class.getClassLoader();        // null(Bootstrap 加载)
com.mysql.jdbc.Driver.class.getClassLoader(); // AppClassLoader 加载
// 通过 ThreadContextClassLoader 实现跨加载器调用

四、如何破坏/自定义双亲委派?

4.1 三种常见方式

方式 实现方式 典型场景 原理
自定义 ClassLoader 重写 loadClass() 破坏委托逻辑 Tomcat、Spring Boot 先尝试自己加载,再委托父加载器
Thread ContextClassLoader Thread.currentThread().setContextClassLoader() JDBC、JNDI、SPI 父加载器调用子加载器
热替换/热部署 创建新 ClassLoader 丢弃旧的 HotSwap、OSGi 同名类不同加载器=不同类

4.2 Tomcat 类加载器结构(破坏双亲委派)

Bootstrap ClassLoaderExtension ClassLoaderApplication ClassLoaderCommon ClassLoaderTomcat/lib(所有 Web 应用共享)

    ├─ Catalina ClassLoaderTomcat 服务器内部类

    ├─ Shared ClassLoader       ← 多个 Web 应用共享

    └─ WebappClassLoader1       ← /WEB-INF/classes & lib(应用隔离)

        WebappClassLoader2      ← 不同版本的同一个库可以共存

Tomcat 加载顺序(破坏双亲委派)

  1. Bootstrap/Extension 加载的类
  2. 先加载 /WEB-INF/classes(优先使用应用自己的类)
  3. 再加载 /WEB-INF/lib/*.jar
  4. 最后才委托给父加载器

为什么这样设计?

  • 🔧 隔离性:不同 Web 应用使用不同版本的 Spring 互不影响
  • 🔄 灵活性:支持热部署,只需替换对应的 WebappClassLoader
  • 性能:避免重复加载同一个库

4.3 SPI 机制(Service Provider Interface)

问题DriverManager 由 Bootstrap 加载,如何加载 classpath 下的 MySQL 驱动?

解决方案:线程上下文类加载器

// DriverManager 源码(精简版)
static {
    loadInitialDrivers();  // 静态块自动执行
}

private static void loadInitialDrivers() {
    // 获取线程上下文类加载器(默认是 AppClassLoader)
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while(driversIterator.hasNext()) {
        driversIterator.next();  // 触发 com.mysql.jdbc.Driver 加载
    }
}

关键代码

// 设置线程上下文类加载器
Thread.currentThread().setContextClassLoader(appClassLoader);

// ServiceLoader 内部使用上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class.forName("com.mysql.jdbc.Driver", false, cl);

五、类加载器相关面试高频题

Q1:双亲委派模型的作用是什么?

  1. 避免类重复加载:同一类只加载一次,通过 findLoadedClass() 缓存
  2. 保证核心 API 不被篡改java.lang.String 只能由 Bootstrap 加载
  3. 保证类型唯一性:同一个 ClassLoader + 全限定名 → 唯一的 Class 对象

Q2:为什么自定义类加载器需要重写 findClass 而不是 loadClass

  • 重写 loadClass彻底破坏双亲委派机制,失去安全保障
  • 推荐只重写 findClass,保留双亲委派的好处
  • loadClass 已经实现了完整的委派逻辑,只需实现"如何查找字节码"的细节

Q3:Class.forName() ClassLoader.loadClass() 的区别?

区别点 Class.forName() ClassLoader.loadClass()
是否初始化 默认执行静态块(可传 initialize=false 不执行初始化(只加载+链接)
使用加载器 当前类的加载器(可指定) 指定 ClassLoader
典型用途 JDBC 驱动加载、Spring IOC 热加载、插件系统、不想触发初始化
方法签名 Class.forName(String name, boolean initialize, ClassLoader loader)

⬅️ JVM 与内存模型 🏠 00-Java ➡️ 垃圾回收与GC算法