编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
        • 2.1 开篇疑问
        • 2.2 类的生命周期全景
        • 2.3 类加载的五个阶段
          • 2.3.1 加载阶段
          • 2.3.2 验证阶段
          • 2.3.3 准备阶段
          • 2.3.4 解析阶段
          • 2.3.5 初始化阶段
          • 2.3.6 clinit与init方法深度解析
        • 2.4 类加载器体系
          • 2.4.1 三层类加载器
          • 2.4.2 类加载器的命名空间
          • 2.4.3 JDK9模块化类加载器变化
        • 2.5 双亲委派模型
          • 2.5.1 什么是双亲委派
          • 2.5.2 源码级原理分析
          • 2.5.3 为什么要设计双亲委派
        • 2.6 打破双亲委派的场景
          • 2.6.1 SPI机制的破局
          • 2.6.2 SPI机制源码深度剖析
          • 2.6.3 OSGi模块化的破局
          • 2.6.4 热部署的破局
          • 2.6.5 Tomcat类加载器架构详解
        • 2.7 自定义类加载器实战
          • 2.7.1 加密类加载器
          • 2.7.2 网络类加载器
          • 2.7.3 热替换类加载器
        • 2.8 类卸载机制
        • 2.9 常见面试深度问题
        • 2.10 总结与核心要点
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

类加载与双亲委派

# 02.类加载与双亲委派

# 目录介绍

  • 2.1 开篇疑问
  • 2.2 类的生命周期全景
  • 2.3 类加载的五个阶段
    • 2.3.1 加载阶段
    • 2.3.2 验证阶段
    • 2.3.3 准备阶段
    • 2.3.4 解析阶段
    • 2.3.5 初始化阶段
    • 2.3.6 clinit与init方法深度解析
  • 2.4 类加载器体系
    • 2.4.1 三层类加载器
    • 2.4.2 类加载器的命名空间
    • 2.4.3 JDK9模块化类加载器变化
  • 2.5 双亲委派模型
    • 2.5.1 什么是双亲委派
    • 2.5.2 源码级原理分析
    • 2.5.3 为什么要设计双亲委派
  • 2.6 打破双亲委派的场景
    • 2.6.1 SPI机制的破局
    • 2.6.2 SPI机制源码深度剖析
    • 2.6.3 OSGi模块化的破局
    • 2.6.4 热部署的破局
    • 2.6.5 Tomcat类加载器架构详解
  • 2.7 自定义类加载器实战
    • 2.7.1 加密类加载器
    • 2.7.2 网络类加载器
    • 2.7.3 热替换类加载器
  • 2.8 类卸载机制
  • 2.9 常见面试深度问题
  • 2.10 总结与核心要点

# 2.1 开篇疑问

疑惑:Java 程序启动时,JVM 是如何找到我们的类并加载进内存的?为什么我们自己写一个 java.lang.String 类不会替换掉 JDK 的 String?JDBC 驱动为什么能自动加载?Tomcat 是怎么实现不同应用部署不同版本 jar 包的?

答疑:这一切的背后,是 JVM 精心设计的类加载机制和双亲委派模型。类加载不仅仅是"把 .class 文件读进内存"这么简单,它涉及到安全隔离、类唯一性保证、模块化等深层设计思想。

# 2.2 类的生命周期全景

一个 Java 类从被加载到虚拟机内存,到卸载出内存,完整生命周期如下:

加载(Loading)
  → 验证(Verification)
    → 准备(Preparation)
      → 解析(Resolution)
        → 初始化(Initialization)
          → 使用(Using)
            → 卸载(Unloading)
1
2
3
4
5
6
7

其中,验证、准备、解析三个阶段统称为链接(Linking)。

触发类加载的时机(有且仅有以下 6 种情况才会触发初始化):

  1. 遇到 new、getstatic、putstatic、invokestatic 字节码指令时
  2. 使用 java.lang.reflect 包对类进行反射调用时
  3. 初始化一个类时,发现其父类还未初始化,先触发父类初始化
  4. JVM 启动时,包含 main() 方法的主类
  5. JDK 7 的动态语言支持(MethodHandle)
  6. 接口定义了 default 方法,实现类初始化时触发接口初始化

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

// 1. 通过子类引用父类的静态字段,只初始化父类
class Father {
    static int value = 100;
    static { System.out.println("Father init"); }
}
class Son extends Father {
    static { System.out.println("Son init"); }
}
// Son.value 只输出 "Father init"

// 2. 通过数组定义引用类,不触发初始化
Father[] arr = new Father[10]; // 不会输出 "Father init"
// JVM 会生成一个 [Lcom.example.Father 的数组类
// 这个数组类由 JVM 自动生成,不是用户定义的

// 3. 常量在编译阶段存入调用类的常量池
class ConstClass {
    static final String NAME = "hello";
    static { System.out.println("ConstClass init"); }
}
// ConstClass.NAME 不会输出 "ConstClass init"
// 因为 "hello" 在编译期已经被存入调用者的常量池

// 4. ClassLoader.loadClass 不触发初始化
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("com.example.MyClass");  // 只加载,不初始化

// 5. Class.forName 指定不初始化
Class.forName("com.example.MyClass", false, cl);  // 第二个参数 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

实验验证:

public class PassiveReference {
    public static void main(String[] args) {
        // 实验1:通过子类引用父类静态字段
        System.out.println(Son.value);  // 只输出 "Father init"
        
        // 实验2:数组
        Father[] arr = new Father[10];  // 没有输出
        
        // 实验3:常量
        System.out.println(ConstClass.NAME);  // 没有输出 "ConstClass init"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.3 类加载的五个阶段

# 2.3.1 加载阶段

加载阶段做三件事:

  1. 通过类的全限定名获取该类的二进制字节流(.class 文件、jar 包、网络、动态代理生成等)
  2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

关键点:JVM 规范没有限定字节流的来源,这为 Java 的灵活扩展提供了空间:

来源 说明 应用场景
本地 .class 文件 最常见的方式 普通应用
ZIP/JAR 包 从压缩文件加载 Maven 依赖
网络获取 从远程服务器下载 Applet、热更新
运行时动态生成 程序运行时生成字节码 动态代理、CGLIB
加密文件 从加密的 class 文件解密加载 代码保护
数据库 从数据库 BLOB 字段读取 特殊部署场景
JSP 文件 JSP 编译为 Servlet class Web 应用

数组类的加载:数组类本身不由类加载器创建,而是由 JVM 直接在内存中动态构造。但数组元素的类型仍需类加载器加载:

// String[] 的类型是 [Ljava.lang.String;
// int[] 的类型是 [I
// String[][] 的类型是 [[Ljava.lang.String;

String[] arr = new String[10];
System.out.println(arr.getClass().getName());  // [Ljava.lang.String;
System.out.println(arr.getClass().getClassLoader());  // null (由Bootstrap加载)
1
2
3
4
5
6
7

# 2.3.2 验证阶段

确保 Class 文件的字节流符合 JVM 规范,不会危害虚拟机安全。

包含四个验证动作:

1. 文件格式验证:

  • 魔数是否为 0xCAFEBABE
  • 主次版本号是否在当前 JVM 处理范围内
  • 常量池中是否有不被支持的常量类型
  • 指向常量的索引值是否有效
任何一个 .class 文件的前4个字节:
CA FE BA BE
1
2

2. 元数据验证(语义分析):

  • 该类是否有父类(除了 Object 外,所有类必须有父类)
  • 该类的父类是否继承了不允许继承的类(final 类)
  • 非抽象类是否实现了父类或接口中要求的所有抽象方法
  • 字段、方法是否与父类矛盾(覆盖了 final 字段等)

3. 字节码验证(最复杂):

  • 操作数栈的数据类型与指令序列匹配
  • 跳转指令不会跳到方法体以外的字节码
  • 方法体中的类型转换有效(不能把 Object 赋值给 int)
// JDK 6 引入了 StackMapTable 来加速字节码验证
// 编译器在 class 文件中记录了类型状态快照
// 验证阶段只需检查快照是否一致,不需要做完整的推导
// 可通过 -XX:-UseSplitVerifier 回退到旧版验证(JDK 7+已移除)
1
2
3
4

4. 符号引用验证:

  • 符号引用中通过全限定名能否找到对应的类
  • 字段、方法的访问性是否可被当前类访问

# 2.3.3 准备阶段

为类的静态变量分配内存并设置零值(注意不是用户赋的值)。

public class PrepareDemo {
    // 准备阶段:value = 0(零值)
    // 初始化阶段:value = 123(用户赋值)
    public static int value = 123;
    
    // 特例:final + static 的常量在编译期就确定
    // 准备阶段直接赋值为 "hello"
    public static final String CONST = "hello";
    
    // 实例变量不在准备阶段分配(随对象创建时分配)
    private int instanceVar = 100;
}
1
2
3
4
5
6
7
8
9
10
11
12

各类型的零值表:

数据类型 零值
int 0
long 0L
short (short) 0
byte (byte) 0
char '\u0000'
float 0.0f
double 0.0d
boolean false
reference null

要点:static 变量在准备阶段设为零值,在初始化阶段执行 <clinit>() 时才赋真实值。而 static final 常量(ConstantValue 属性)在准备阶段直接赋最终值。

ConstantValue 的限制:

// 只有同时满足 static + final + 基本类型或String 才使用 ConstantValue
public static final int A = 100;       // ConstantValue,准备阶段赋值
public static final String B = "hi";   // ConstantValue,准备阶段赋值
public static final Integer C = 100;   // 不是 ConstantValue!初始化阶段赋值
public static final Object D = null;   // 不是 ConstantValue!初始化阶段赋值
public static final int E = getValue();// 不是 ConstantValue!需要运行方法
1
2
3
4
5
6

# 2.3.4 解析阶段

将常量池中的符号引用替换为直接引用。

  • 符号引用:用一组符号来描述引用的目标(如 java/lang/Object),与内存布局无关
  • 直接引用:可以是指向目标的指针、相对偏移量或能间接定位到目标的句柄,与内存布局直接相关
常量池中的符号引用:
#2 = Class              #25           // java/lang/Object
#3 = Methodref          #2.#26        // java/lang/Object."<init>":()V
#25 = Utf8              java/lang/Object
#26 = NameAndType       #12:#13       // "<init>":()V

解析后 → 替换为指向方法区中 Object 类数据的直接指针
1
2
3
4
5
6
7

四类符号引用的解析:

  1. 类或接口的解析:将类名解析为 Class 对象引用
  2. 字段解析:先解析所属类,再在类及其父类/接口中查找字段
  3. 方法解析:先解析所属类,再在类及其父类中查找方法
  4. 接口方法解析:先解析所属接口,再在接口及其父接口中查找
// 字段解析的查找顺序
// 1. 在本类中查找
// 2. 在实现的接口及父接口中递归查找
// 3. 在父类中递归查找
// 4. 都找不到 → NoSuchFieldError

// 如果在接口和父类中都找到了同名字段 → 编译器报 ambiguous 错误
interface I { int x = 1; }
class Father { int x = 2; }
class Son extends Father implements I {
    // 使用 x 会编译报错:reference to x is ambiguous
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.3.5 初始化阶段

执行类构造器 <clinit>() 方法的过程。<clinit>() 由编译器自动收集类中所有静态变量的赋值动作和静态代码块合并产生。

public class ClinitDemo {
    static int a = 1;              // 收集
    static { a = 2; }             // 收集
    static int b = a;             // 收集
    // <clinit>() 按顺序执行后:a=2, b=2
}
1
2
3
4
5
6

关键特性:

  1. 线程安全:JVM 保证 <clinit>() 在多线程环境下被正确加锁同步,只执行一次
  2. 父类优先:子类的 <clinit>() 执行前,父类的 <clinit>() 一定已执行完
  3. 非必需:如果类没有静态变量赋值和静态代码块,编译器不会生成 <clinit>()
  4. 接口差异:接口也有 <clinit>(),但执行接口的 <clinit>() 不会触发父接口的 <clinit>()
// 基于类初始化的线程安全单例
public class Singleton {
    private Singleton() {}
    
    // 静态内部类在首次使用时才加载,JVM 保证 <clinit>() 线程安全
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

利用 <clinit>() 的线程安全特性进行死锁:

// 如果两个类的 <clinit>() 互相依赖,可能产生死锁
class A {
    static {
        System.out.println("A clinit start");
        try { Thread.sleep(100); } catch (Exception e) {}
        new B();  // 触发 B 的初始化
        System.out.println("A clinit end");
    }
}

class B {
    static {
        System.out.println("B clinit start");
        new A();  // 触发 A 的初始化
        System.out.println("B clinit end");
    }
}

// 线程1加载A → A的<clinit>()持锁 → 需要B初始化
// 线程2加载B → B的<clinit>()持锁 → 需要A初始化
// 结果:死锁!两个线程永远阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.3.6 clinit与init方法深度解析

JVM 中有两个特殊方法:

方法 全名 触发时机 内容
<clinit> 类构造器 类初始化(只执行一次) 静态变量赋值 + 静态代码块
<init> 实例构造器 每次 new 对象 实例变量赋值 + 代码块 + 构造器
public class InitOrder {
    // 以下是 <clinit>() 的内容
    static int staticVar = 10;
    static { System.out.println("static block: " + staticVar); }
    
    // 以下是 <init>() 的内容
    int instanceVar = 20;
    { System.out.println("instance block: " + instanceVar); }
    
    public InitOrder() {
        System.out.println("constructor");
    }
    
    public static void main(String[] args) {
        new InitOrder();
        new InitOrder();
    }
}

// 输出:
// static block: 10        ← <clinit>(),只执行一次
// instance block: 20      ← <init>(),第一次new
// constructor
// instance block: 20      ← <init>(),第二次new
// constructor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

父子类的初始化顺序:

1. 父类 <clinit>()(静态变量 + 静态代码块)
2. 子类 <clinit>()(静态变量 + 静态代码块)
3. 父类 <init>()(实例变量 + 代码块 + 构造器)
4. 子类 <init>()(实例变量 + 代码块 + 构造器)
1
2
3
4

# 2.4 类加载器体系

# 2.4.1 三层类加载器

┌─────────────────────────────────┐
│  Bootstrap ClassLoader (启动类)   │  ← C++ 实现,加载 rt.jar
│  加载路径: JAVA_HOME/lib          │
├─────────────────────────────────┤
│  Extension ClassLoader (扩展类)   │  ← Java 实现
│  加载路径: JAVA_HOME/lib/ext      │
├─────────────────────────────────┤
│  Application ClassLoader (应用类) │  ← Java 实现
│  加载路径: classpath              │
├─────────────────────────────────┤
│  User ClassLoader (自定义类)      │  ← 用户自定义
│  加载路径: 自定义                  │
└─────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

各加载器的详细说明:

加载器 实现语言 加载路径 说明
Bootstrap C++ $JAVA_HOME/lib(rt.jar等) JVM 的一部分,不继承 ClassLoader
Extension Java $JAVA_HOME/lib/ext 或 java.ext.dirs sun.misc.Launcher$ExtClassLoader
Application Java classpath / -cp sun.misc.Launcher$AppClassLoader
自定义 Java 自定义路径 继承 ClassLoader
public class ClassLoaderDemo {
    public static void main(String[] args) {
        // String 由 Bootstrap 加载,返回 null
        System.out.println(String.class.getClassLoader());
        // null
        
        // 应用类由 AppClassLoader 加载
        System.out.println(ClassLoaderDemo.class.getClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        
        // 查看加载器层级
        ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl);
            cl = cl.getParent();
        }
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        // sun.misc.Launcher$ExtClassLoader@1b6d3586
        // null (Bootstrap,C++实现,Java中表示为null)
        
        // 查看 Bootstrap 加载了哪些 jar
        String bootstrapPath = System.getProperty("sun.boot.class.path");
        System.out.println(bootstrapPath);
        // /Library/Java/jdk1.8/jre/lib/rt.jar:...
        
        // 查看 Extension 加载了哪些路径
        String extPath = System.getProperty("java.ext.dirs");
        System.out.println(extPath);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 2.4.2 类加载器的命名空间

核心原则:在 JVM 中,判断两个类是否"相同",不仅要看类的全限定名,还要看加载它的类加载器。同一个 .class 文件被不同的类加载器加载,会产生两个不同的类。

// 不同类加载器加载同一个类
ClassLoader cl1 = new MyClassLoader("/path/to/classes");
ClassLoader cl2 = new MyClassLoader("/path/to/classes");

Class<?> c1 = cl1.loadClass("com.example.Foo");
Class<?> c2 = cl2.loadClass("com.example.Foo");

System.out.println(c1 == c2);           // false ← 不同的 Class 对象
System.out.println(c1.equals(c2));      // false
1
2
3
4
5
6
7
8
9

这个特性是实现类隔离(如 Tomcat 部署多个 Web 应用、OSGi 模块化)的基础。

类的唯一标识 = 全限定名 + 类加载器:

// 即使是同一个 .class 文件
// cl1 加载的 com.example.Foo 和 cl2 加载的 com.example.Foo
// 是两个完全不同的类型
// 它们的实例不能互相赋值,不能互相 instanceof

Object obj = c1.newInstance();
System.out.println(c2.isInstance(obj));  // false
// obj 是 cl1 加载的 Foo 的实例,不是 cl2 加载的 Foo 的实例
1
2
3
4
5
6
7
8

# 2.4.3 JDK9模块化类加载器变化

JDK 9 引入模块化系统(JPMS),类加载器体系发生了重大变化:

JDK 8:
  Bootstrap → Extension → Application

JDK 9+:
  Bootstrap → Platform → Application
1
2
3
4
5
JDK 8 JDK 9+ 变化
Bootstrap ClassLoader Bootstrap ClassLoader 加载 java.base 等基础模块
Extension ClassLoader Platform ClassLoader 改名,不再有 ext 目录
Application ClassLoader Application ClassLoader 加载模块路径和类路径
// JDK 9+ 获取 Platform ClassLoader
ClassLoader platform = ClassLoader.getPlatformClassLoader();

// JDK 9+ 类加载器的实现类也变了
// Bootstrap: 仍由 C++ 实现
// Platform: jdk.internal.loader.ClassLoaders$PlatformClassLoader
// Application: jdk.internal.loader.ClassLoaders$AppClassLoader
// 它们不再继承 URLClassLoader(影响了很多通过反射操作类加载器的框架)
1
2
3
4
5
6
7
8

模块路径 vs 类路径:

# JDK 8: 类路径
java -cp lib/*.jar com.example.Main

# JDK 9+: 模块路径(优先)+ 类路径(兼容)
java --module-path lib -m my.module/com.example.Main

# 未模块化的 jar 放在类路径上,属于 "unnamed module"
# 命名模块需要 opens 才能被反射访问
1
2
3
4
5
6
7
8

# 2.5 双亲委派模型

# 2.5.1 什么是双亲委派

双亲委派模型(Parents Delegation Model)定义了类加载器之间的协作关系:

当一个类加载器收到类加载请求时,它首先不会自己去尝试加载,而是把请求委派给父类加载器。每一层都是如此,直到顶层的 Bootstrap ClassLoader。只有当父加载器反馈无法完成加载(在其搜索范围内找不到对应的类),子加载器才尝试自己加载。

加载请求: com.example.MyClass
  → AppClassLoader: 我先不加载,问问父亲
    → ExtClassLoader: 我也先不加载,问问父亲
      → Bootstrap: 我在 rt.jar 里找不到,我加载不了
    ← ExtClassLoader: 我在 ext 目录也找不到,加载不了
  ← AppClassLoader: 好,那我来加载,在 classpath 里找到了
1
2
3
4
5
6

# 2.5.2 源码级原理分析

双亲委派的实现逻辑在 ClassLoader.loadClass() 方法中,核心代码清晰而优雅:

// java.lang.ClassLoader#loadClass 核心逻辑
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 第一步:检查类是否已经加载过
        Class<?> c = findLoadedClass(name);
        
        if (c == null) {
            try {
                // 第二步:委派给父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 父加载器为null,说明是Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器无法完成加载,不做处理
            }
            
            if (c == null) {
                // 第三步:父加载器无法加载,自己来
                c = findClass(name);
            }
        }
        
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

设计精髓:用不到 30 行代码实现了一个层级分明、责任清晰的加载体系。自定义类加载器只需重写 findClass() 方法即可融入这个体系。

getClassLoadingLock 的并发控制:

// JDK 7+ 引入了并行类加载能力
// 每个类名对应一个独立的锁,不同类可以并行加载
protected Object getClassLoadingLock(String className) {
    Object lock = this;  // 默认以 ClassLoader 实例为锁
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) lock = newLock;
    }
    return lock;
}
// 开启并行加载:ClassLoader.registerAsParallelCapable()
1
2
3
4
5
6
7
8
9
10
11
12

# 2.5.3 为什么要设计双亲委派

安全性:防止核心 API 被篡改。即使你写了一个 java.lang.String 类,它也不会被加载,因为请求会先到 Bootstrap ClassLoader,它会加载 rt.jar 中的 String。

唯一性:保证同一个类在 JVM 中只被加载一次。所有的加载请求最终都会汇聚到顶层,由负责该类的加载器统一加载。

稳定性:基础类库由高层加载器统一加载,避免了混乱的类加载顺序导致的 ClassCastException。

// 尝试自定义 java.lang.String(无效,双亲委派保护)
package java.lang;
public class String {
    public static void main(String[] args) {
        System.out.println("自定义的String");
    }
}
// 运行结果:错误!在类 java.lang.String 中找不到 main 方法
// 因为 Bootstrap 加载的是 rt.jar 中的 String,没有 main 方法
1
2
3
4
5
6
7
8
9

更深层的安全机制:即使绕过了双亲委派,JVM 还有一道防线——java.lang 包的保护:

// 即使用自定义类加载器加载 java.lang.XXX
// JVM 在 defineClass 时也会检查包名
// 以 java. 开头的包名只允许 Bootstrap ClassLoader 加载
// 否则抛出 SecurityException: Prohibited package name: java.lang
1
2
3
4

# 2.6 打破双亲委派的场景

双亲委派不是强制约束,某些场景下需要打破它。

# 2.6.1 SPI机制的破局

疑惑:JDBC 的 DriverManager 在 rt.jar 中,由 Bootstrap ClassLoader 加载。但具体的驱动实现(如 MySQL 驱动)在 classpath 中,需要 AppClassLoader 才能加载。Bootstrap 怎么加载 classpath 的类?

论证:这就是经典的 SPI(Service Provider Interface)问题。解决方案是线程上下文类加载器(Thread Context ClassLoader)。

// DriverManager 核心加载逻辑
// rt.jar 中的代码,通过线程上下文类加载器"向下"加载
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

// ServiceLoader.load 内部实现
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器(默认是 AppClassLoader)
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
1
2
3
4
5
6
7
8
9
10

本质:父加载器请求子加载器去完成类加载,打破了自下而上的委派链。

# 2.6.2 SPI机制源码深度剖析

SPI 的完整流程:

1. 接口定义在核心库(如 java.sql.Driver)
2. 实现在第三方 jar(如 mysql-connector-java.jar)
3. 第三方 jar 的 META-INF/services/java.sql.Driver 文件中写实现类名
4. ServiceLoader 读取文件,用线程上下文类加载器加载实现类
1
2
3
4
// META-INF/services/java.sql.Driver 文件内容:
// com.mysql.cj.jdbc.Driver

// ServiceLoader 核心逻辑
public final class ServiceLoader<S> implements Iterable<S> {
    
    private class LazyIterator implements Iterator<S> {
        // 读取 META-INF/services/ 下的配置文件
        private boolean hasNextService() {
            String fullName = PREFIX + service.getName();
            // PREFIX = "META-INF/services/"
            // fullName = "META-INF/services/java.sql.Driver"
            configs = loader.getResources(fullName);
            // 解析文件内容,获取实现类名
            // "com.mysql.cj.jdbc.Driver"
        }
        
        private S nextService() {
            String cn = nextName;
            // 用线程上下文类加载器加载实现类
            Class<?> c = Class.forName(cn, false, loader);
            // 实例化
            S p = service.cast(c.newInstance());
            return p;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

JDBC 4.0+ 自动加载驱动:

// JDK 6+ 不再需要 Class.forName("com.mysql.cj.jdbc.Driver")
// DriverManager 的静态代码块中会通过 SPI 自动发现驱动

// DriverManager.java
static {
    loadInitialDrivers();
}

private static void loadInitialDrivers() {
    // 通过 SPI 加载所有 java.sql.Driver 实现
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while (driversIterator.hasNext()) {
        driversIterator.next();  // 加载并实例化驱动
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2.6.3 OSGi模块化的破局

OSGi 实现了模块化热部署,每个 Bundle(模块)有自己的类加载器,形成网状结构而非树状结构。类加载变成:

  1. java.* 开头的类委派给父加载器
  2. 在委派列表(Import-Package)中的类委派给对应 Bundle 的加载器
  3. 自己的 classpath 内的类由自己的加载器加载
传统双亲委派(树状):          OSGi(网状):
    Bootstrap                    Bootstrap
       ↑                         ↗  ↑  ↖
      Ext                    Bundle1 Bundle2 Bundle3
       ↑                       ↕      ↕
      App                    Bundle4  Bundle5
1
2
3
4
5
6

# 2.6.4 热部署的破局

Tomcat 需要支持多个 Web 应用部署不同版本的同名类,因此每个 Web 应用有独立的 WebAppClassLoader,加载顺序是先尝试自己加载,再委派给父加载器——与双亲委派相反。

# 2.6.5 Tomcat类加载器架构详解

┌─────────────────────────────────────┐
│        Bootstrap ClassLoader         │  JVM 核心类
├─────────────────────────────────────┤
│        System ClassLoader            │  CLASSPATH
├─────────────────────────────────────┤
│        Common ClassLoader            │  Tomcat 和所有 WebApp 共享
│        (catalina.properties)         │  如: Servlet API
├──────────────┬──────────────────────┤
│ Catalina CL  │  Shared ClassLoader   │  Tomcat 自身 / WebApp 共享
├──────────────┼──────────────────────┤
│              │  WebApp1 ClassLoader   │  ← WebApp1 独立
│              │  WebApp2 ClassLoader   │  ← WebApp2 独立
│              │  (WEB-INF/classes,lib) │
├──────────────┼──────────────────────┤
│              │  Jsp ClassLoader       │  ← 每个 JSP 一个加载器
└──────────────┴──────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

WebAppClassLoader 的加载顺序(打破双亲委派):

1. 先检查缓存(是否已加载过)
2. 检查是否是 JVM 核心类(java.* javax.*),如果是 → 委派给父加载器
3. 尝试自己加载(WEB-INF/classes → WEB-INF/lib)  ← 先自己加载!
4. 自己找不到 → 委派给父加载器(Common ClassLoader)
1
2
3
4
// WebAppClassLoader.loadClass 的简化逻辑
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1. 缓存检查
    Class<?> clazz = findLoadedClass(name);
    if (clazz != null) return clazz;
    
    // 2. JVM 核心类,必须委派给父加载器
    if (name.startsWith("java.") || name.startsWith("javax.servlet.")) {
        return super.loadClass(name, resolve);
    }
    
    // 3. 先自己加载(打破双亲委派!)
    try {
        clazz = findClass(name);  // WEB-INF/classes 和 WEB-INF/lib
        if (clazz != null) return clazz;
    } catch (ClassNotFoundException e) {
        // 找不到,继续
    }
    
    // 4. 自己找不到,再委派给父加载器
    return super.loadClass(name, resolve);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

为什么 Tomcat 要打破双亲委派:

需求 双亲委派的问题 Tomcat 的解决
应用隔离 不同应用的同名类会冲突 每个 WebApp 独立的类加载器
版本共存 同一个类只能加载一个版本 不同 WebApp 加载不同版本
热部署 类一旦加载无法卸载 销毁旧 CL,创建新 CL
共享类 每个应用都加载一份浪费内存 Common CL 加载共享类

# 2.7 自定义类加载器实战

# 2.7.1 加密类加载器

当需要从非标准来源加载类(加密文件、网络、数据库等)时,可以自定义类加载器:

public class EncryptClassLoader extends ClassLoader {
    private String classDir;
    
    public EncryptClassLoader(String classDir) {
        this.classDir = classDir;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 1. 读取加密的 class 文件
            String fileName = classDir + "/" + name.replace('.', '/') + ".class";
            byte[] encryptedBytes = Files.readAllBytes(Paths.get(fileName));
            
            // 2. 解密字节码
            byte[] classBytes = decrypt(encryptedBytes);
            
            // 3. 将字节数组转为 Class 对象
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
    
    private byte[] decrypt(byte[] data) {
        // 解密逻辑(示例:简单异或)
        byte[] result = new byte[data.length];
        for (int i = 0; i < data.length; i++) {
            result[i] = (byte) (data[i] ^ 0x5A);
        }
        return result;
    }
}

// 使用
EncryptClassLoader loader = new EncryptClassLoader("/path/to/encrypted");
Class<?> clazz = loader.loadClass("com.example.Secret");
Object obj = clazz.getDeclaredConstructor().newInstance();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

关键点:重写 findClass() 而不是 loadClass(),这样既能实现自定义加载逻辑,又能保留双亲委派机制。

# 2.7.2 网络类加载器

public class NetworkClassLoader extends ClassLoader {
    private String baseUrl;
    
    public NetworkClassLoader(String baseUrl) {
        this.baseUrl = baseUrl;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            String url = baseUrl + "/" + name.replace('.', '/') + ".class";
            URL classUrl = new URL(url);
            
            try (InputStream is = classUrl.openStream();
                 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[4096];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    bos.write(buffer, 0, len);
                }
                byte[] classBytes = bos.toByteArray();
                return defineClass(name, classBytes, 0, classBytes.length);
            }
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }
}

// 从远程服务器加载类
NetworkClassLoader loader = new NetworkClassLoader("https://cdn.example.com/classes");
Class<?> clazz = loader.loadClass("com.example.RemotePlugin");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 2.7.3 热替换类加载器

public class HotSwapClassLoader extends ClassLoader {
    private String classDir;
    
    public HotSwapClassLoader(String classDir, ClassLoader parent) {
        super(parent);
        this.classDir = classDir;
    }
    
    // 重写 loadClass 打破双亲委派
    // 每次都重新加载最新版本的 class 文件
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 核心类仍走双亲委派
        if (name.startsWith("java.") || name.startsWith("javax.")) {
            return super.loadClass(name);
        }
        
        // 自定义类:每次从磁盘重新加载
        String fileName = classDir + "/" + name.replace('.', '/') + ".class";
        Path path = Paths.get(fileName);
        if (Files.exists(path)) {
            try {
                byte[] bytes = Files.readAllBytes(path);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException e) {
                throw new ClassNotFoundException(name, e);
            }
        }
        
        return super.loadClass(name);
    }
}

// 热替换流程
public class HotSwapDemo {
    public static void main(String[] args) throws Exception {
        while (true) {
            // 每次创建新的类加载器(旧的被 GC 回收后,旧类也会被卸载)
            HotSwapClassLoader loader = new HotSwapClassLoader(
                "/path/to/classes", ClassLoader.getSystemClassLoader());
            
            // 加载最新版本的类
            Class<?> clazz = loader.loadClass("com.example.Plugin");
            Object plugin = clazz.getDeclaredConstructor().newInstance();
            
            // 通过反射调用(因为每次加载的 Class 对象不同,不能强转)
            Method method = clazz.getMethod("execute");
            method.invoke(plugin);
            
            Thread.sleep(5000);  // 每5秒重新加载
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 2.8 类卸载机制

类卸载需要同时满足三个条件:

  1. 该类的所有实例都已被 GC 回收
  2. 加载该类的 ClassLoader 已被 GC 回收
  3. 该类对应的 java.lang.Class 对象没有任何引用
// Bootstrap/Extension/Application 类加载器加载的类永远不会被卸载
// 因为这三个类加载器的生命周期与 JVM 相同

// 只有自定义类加载器加载的类才可能被卸载
MyClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("com.example.Foo");
Object obj = clazz.newInstance();

// 要卸载 Foo 类:
obj = null;     // 清除所有实例引用
clazz = null;   // 清除 Class 引用
loader = null;  // 清除类加载器引用
System.gc();    // 建议 GC(不保证立即执行)
1
2
3
4
5
6
7
8
9
10
11
12
13

验证类卸载:

# 通过 JVM 参数查看类卸载
-verbose:class
# 或
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
1
2
3
4
5
[Loaded com.example.Foo from file:/path/to/classes/]
...
[Unloading class com.example.Foo 0x00000007c0060828]
1
2
3

# 2.9 常见面试深度问题

Q1:能不能自己写一个 java.lang.System 类?

不能。即使你写了,也不会被加载。原因有两层:

  1. 双亲委派:Bootstrap ClassLoader 优先加载 rt.jar 中的 System
  2. 安全检查:即使打破双亲委派,defineClass 会检查包名,拒绝 java. 开头的类

Q2:为什么 JDK 9 去掉了 Extension ClassLoader?

JDK 9 引入模块化后,不再需要 ext 目录这种松散的扩展机制。所有标准模块都通过模块路径管理,更加安全可控。Platform ClassLoader 替代了 Extension ClassLoader,负责加载 Java SE 的标准模块。

Q3:ClassNotFoundException 和 NoClassDefFoundError 的区别?

ClassNotFoundException NoClassDefFoundError
类型 Exception(受检异常) Error(错误)
触发 Class.forName() / ClassLoader.loadClass() 找不到类 编译时存在,运行时找不到
原因 类路径配置错误,jar 包缺失 类初始化失败,jar 包版本冲突
举例 反射加载的类不存在 类的静态代码块抛异常
// ClassNotFoundException
Class.forName("com.example.NotExist");  // 类不存在

// NoClassDefFoundError
// 假设 A 的 static {} 中抛异常
class A { static { int x = 1/0; } }
try {
    new A();  // ExceptionInInitializerError
} catch (Throwable t) {}
new A();  // NoClassDefFoundError(类初始化已失败,不会再次初始化)
1
2
3
4
5
6
7
8
9
10

Q4:loadClass 和 forName 的区别?

// Class.forName("xxx") 会执行类的初始化(static 块执行)
Class.forName("com.mysql.cj.jdbc.Driver");
// → 加载 + 链接 + 初始化

// ClassLoader.loadClass("xxx") 只执行加载,不初始化
ClassLoader.getSystemClassLoader().loadClass("com.mysql.cj.jdbc.Driver");
// → 只加载,不执行 static 块
1
2
3
4
5
6
7

# 2.10 总结与核心要点

类加载机制的设计哲学:

  1. 延迟加载:类在首次主动使用时才加载和初始化,节约资源
  2. 安全隔离:双亲委派保证核心类不被篡改,类加载器命名空间实现类隔离
  3. 灵活扩展:自定义类加载器 + SPI 机制让 Java 具备了强大的动态扩展能力
  4. 线程安全:<clinit>() 的加锁机制保证了类初始化的线程安全
  5. 可卸载:自定义类加载器加载的类可以被卸载,支持热部署

技术演变总结:

阶段 特点 代表技术
JDK 1.0 基础双亲委派 三层类加载器
JDK 1.2 引入线程上下文 CL SPI 机制
JDK 1.4+ 打破双亲委派 OSGi、Tomcat
JDK 7 并行类加载 registerAsParallelCapable
JDK 9+ 模块化系统 Jigsaw,Platform CL 替代 Extension CL

核心要点速查:

问题 答案
双亲委派的作用 安全性(防篡改)+ 唯一性(防重复加载)
如何打破双亲委派 重写 loadClass() 而不是 findClass()
如何自定义类加载器 继承 ClassLoader,重写 findClass()
SPI 如何突破委派 线程上下文类加载器(父加载器"向下"委托)
类卸载条件 实例+Class+ClassLoader 全部无引用
Tomcat 类加载顺序 先自己加载 → 再委派父加载器
clinit 线程安全原因 JVM 对 () 加锁,保证只执行一次

理解类加载机制,是理解 Java 动态特性(反射、动态代理、热部署、SPI)的根基。

上次更新: 2026/06/10, 11:13:41
JVM内存模型与对象
垃圾回收与GC调优

← JVM内存模型与对象 垃圾回收与GC调优→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式