(15)《类加载器》

2026-03-21
2
-
- 分钟
|

核心原则:类加载器像“快递员”,双亲委派是“先问上级有没有,没得才自己取”——避免重复派送,保证安全。


1. 概述:类加载器层级

(从上到下,父类加载器 → 子类加载器)

  • Bootstrap ClassLoader(根加载器,C++实现):
    • 加载路径:JAVA_HOME/jre/lib(核心JDK类,如 rt.jar)。
    • 特点:无法在Java中直接访问(JVM内置)。
  • ExtClassLoader(扩展加载器):
    • 加载路径:JAVA_HOME/jre/lib/ext(JDK扩展类)。
    • 父类:Bootstrap(显示为 null,因无父级)。
  • AppClassLoader(应用加载器,系统默认):
    • 加载路径:classpath(项目代码、依赖Jar)。
    • 父类:ExtClassLoader
  • 自定义类加载器
    • 加载路径:自定义(如网络、数据库、特定目录)。
    • 父类:AppClassLoader

💡 通俗比喻
Bootstrap 是“邮局总部”,Ext 是“区域分站”,App 是“本地快递员”。自定义类加载器是“私人快递员”,必须先问总部/分站有没有包裹(类),没得才自己取。


2. 双亲委派机制

核心过程loadClass 方法逻辑):

  1. 先查自身是否加载过 → 2. 递归问父类加载器 → 3. 直到 Bootstrap → 4. 若全没加载,调用 findClass() 加载。

为什么用双亲委派?(重点!)

  • 避免类重复加载
    JVM 用 类加载器 + 类名 区分类。
    → 例:ClassLoader AClassLoader B 都加载 com.example.User → JVM 视为 两个不同类
    后果ClassCastException(类型转换异常)。
  • 安全保证
    核心类(如 java.lang.String)由 Bootstrap 加载,防止被恶意覆盖(如 String 被改写)。

能否打破双亲委派?

  • 可以,但需谨慎(常见于框架/特殊场景)。
  • 为什么打破?
    某些类(如 JDBC 驱动)需由 应用类加载器 加载,但接口由 Bootstrap 加载 → 无法直接加载实现类。

💡 通俗解释
双亲委派 = “先问总部有没有,没得才自己去取”。
打破 = “总部说‘我不会取,你(线程上下文)去取’”。


3. 线程上下文类加载器(JDBC 例子)

问题

  • DriverManagerBootstrap 加载(核心类),但 MySQL 驱动在 classpath(不在 jre/lib),Bootstrap 无法加载。

解决方案ServiceLoader 机制):

  1. DriverManager 静态块中调用 ServiceLoader.load(Driver.class)

  2. ServiceLoader 主动使用线程上下文类加载器

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    cl.loadClass("com.mysql.jdbc.Driver"); // 用 AppClassLoader 加载
    
  3. 为什么能这样?

    • JDK 接口(如 java.sql.Driver)由 Bootstrap 加载 → 但实现类(如 MySQL 驱动)在第三方 Jar → Bootstrap 无法加载
    • 线程上下文类加载器(默认是 AppClassLoader)能访问 classpath,所以反向委派加载。

💡 通俗比喻
Bootstrap 说:“接口我管,但实现类你(线程)自己去取!” → 线程用自己带的“钥匙”(AppClassLoader)打开第三方仓库。


4. 自定义类加载器

何时需要?

  1. 加载非 classpath 的类(如从网络/数据库动态加载)。
  2. 解耦设计(通过接口调用实现,框架常用)。
  3. 类隔离(Tomcat 为每个 Web 应用用独立类加载器 → 同名类不冲突)。

正确实现步骤(避免破坏双亲委派):

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) { // ✅ 重点:必须重写 findClass,不是 loadClass
        byte[] classBytes = loadClassBytes(name); // 从自定义路径读取字节码
        return defineClass(name, classBytes, 0, classBytes.length); // 交给父类加载
    }
}

关键点

  • ❌ 错误:重写 loadClass() → 破坏双亲委派(JVM 会直接调用 findClass,不再递归上级)。
  • ✅ 正确:重写 findClass() → 保证先走双亲委派,最后才自己加载。

💡 一句话总结
“继承 ClassLoader → 重写 findClass() → 读字节码 → 调用 defineClass() → 用 loadClass() 加载类”。


最后提醒

  • 双亲委派是 JVM 安全基石 → 除非必要(如 SPI),否则别打破。
  • 自定义类加载器 = “定制快递员”,必须遵守“先问上级”规则!
评论交流

文章目录