目录

  1. 概览

  2. 关键概念与角色

  3. 设计目标与动机

  4. loadClass 的典型实现与加载流程

  5. findClass 与 defineClass 的协作

  6. 类身份的运行时判断规则

  7. 三类破坏双亲委派的典型场景

  8. 工程实践建议与排查要点

  9. 常见故障

  10. 小结

1.概览

双亲委派模型是 Java 类加载器的一种核心加载策略。它规定类加载请求在加载器链中如何向上委托,从而保证同一个限定名的类尽可能由同一个加载器完成定义,避免命名冲突与运行时类型不一致问题。本文以源码要点和实现细节为主线,讲清模型的目标、流程、实现与常见破坏场景,并给出工程实践建议,方便直接用于技术博客发布。

2.关键概念与角色

Bootstrap ClassLoader 引导加载器,由 JVM 实现,负责加载 JRE 核心类库(在 Java 层通常表现为 parent == null)。

Extension ClassLoader 扩展加载器,负责加载 jre/lib/ext 或扩展目录下的类。

Application ClassLoader 应用加载器,负责加载应用 classpath 下的类,通常是线程上下文类加载器的默认值。

自定义 ClassLoader 开发者继承 java.lang.ClassLoader,实现 findClass,从自定义来源读取字节流。

parent 字段 表示委托目标,不是继承关系,而是组合关系。

defineClass 方法final 的,将字节流变成 Class 对象,后续验证、准备、解析、初始化由 JVM 或基础类库统一完成。

3.设计目标与动机

双亲委派模型的核心目标是保证类的唯一性:在被动情况下,遇到某限定名的类,加载请求应优先由更上层的加载器处理,只有父加载器无法找到时,子加载器才尝试加载。这样可以:

优先使用核心类库实现,避免被应用或第三方 jar 覆盖导致安全或一致性问题。

减少命名空间冲突,保证类型判断一致性,避免 ClassCastException 等问题。

4.loadClass 的典型实现与加载流程

java.lang.ClassLoader.loadClass 是双亲委派逻辑的所在。下面是简化的伪代码,便于理解流程:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查缓存
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 2. 委托父加载器
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                // parent == null 表示 bootstrap 层
                c = findBootstrapClassOrNull(name);
            }
            // 3. 父加载失败,子加载器尝试
            if (c == null) {
                c = findClass(name); // 由具体加载器实现如何读取字节流
            }
        }
        if (resolve) resolveClass(c);
        return c;
    }
}

要点说明

findLoadedClass 命中已加载缓存,避免重复定义。

parent == null 表示到达 bootstrap 层,其查找由 JVM 内部实现完成。

findClass 由具体 ClassLoader 实现,例如 URLClassLoader 通过 URLClassPath 查找资源再调用 defineClass

defineClassfinal,把字节流变为 Class 的后续步骤由统一机制处理,保证一致性和安全性。

5.findClass 与 defineClass 的协作

URLClassLoader 为例,findClass 的工作流程可以概括为:把限定名转换为路径,从资源集合中查找 class 文件,读取字节数组后交给 defineClass

示例伪码:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    String path = name.replace('.', '/').concat(".class");
    Resource res = ucp.getResource(path, false);
    if (res != null) {
        byte[] bytes = res.getBytes();
        return defineClass(name, bytes, 0, bytes.length, protectionDomain);
    }
    throw new ClassNotFoundException(name);
}

设计意图

各类加载器负责如何获取字节流,不负责把字节流变为运行时类对象的细节。

统一的 defineClass 与 JVM 层处理后续步骤,既简化实现也保证安全与一致性。

6.类身份的运行时判断规则

在 JVM 中,一个类的身份由三要素决定:

  1. 完全限定名(fully qualified name)

  2. 加载该类的类加载器

  3. 保护域(ProtectionDomain)和来源等运行时属性

因此,即便两个类字节码完全相同,只要由不同类加载器加载,它们也被视为不同类型,这就是发生 ClassCastException 的根本原因。

7.三类破坏双亲委派的典型场景

尽管双亲委派是默认且保守的策略,但在工程实践中存在多种会破坏或绕过该策略的情况,下面列出三种典型场景并解释原因与影响。

  (一) 主动覆盖 loadClass

开发者可以直接重写 ClassLoader.loadClass 并改变委托顺序。如果子加载器优先加载某些类,就会导致同名类在不同加载器中重复定义,产生命名空间分离。该问题难以调试并会引发类型不一致等错误。

建议:避免重写 loadClass,必要时通过重写 findClass 来实现自定义加载逻辑,保留父委托语义。

(二) SPI ServiceLoader 及 JDK 扩展点的向下查找

JDK 的 SPI 机制(例如 ServiceLoaderDriverManager)在加载第三方实现时通常使用线程上下文类加载器(Thread Context ClassLoader)。这意味着核心库代码在运行时会触发应用类加载器去加载实现,从“调用关系”上看是上层触发下层加载,从而在逻辑上破坏了自上而下的严格委派。这是为支持可插拔扩展而做出的折中设计。

(三)热部署、插件与模块化的动态加载

容器或热重载框架会在运行期频繁创建与销毁自定义类加载器,并加载不同版本的类,导致 JVM 内存在多个同名类的定义。若没有清晰的隔离与回收策略,会造成内存泄漏和类型不匹配。

8.工程实践建议与排查要点

避免重写 loadClass,除非非常必要。优先选择重写 findClass,并保持父委派语义。

明确模块边界:在容器或插件架构中设计好哪些包由父加载、哪些包由子加载。

谨慎使用线程上下文类加载器:使用时显式设置并在完成后恢复,避免 ServiceLoader 加载到意外实现。

热部署注意资源释放:释放线程、静态引用、监听器、线程本地变量,确保类加载器可回收。

调试技巧:打印类加载器来源 SomeClass.class.getClassLoader() 或查看 ProtectionDomain 的来源信息以定位问题。

9.常见故障

ClassCastException
检查相关类的 getClassLoader() 是否不同;查找是否存在重复 jar 或自定义类加载器。

NoClassDefFoundError / ClassNotFoundException
检查 classpath、模块边界与线程上下文类加载器设置;确认服务描述文件(META-INF/services)是否正确。

内存泄漏(热部署后不回收)
检查静态字段、线程池、事件监听器、线程本地变量是否持有类加载器或类引用。

SPI 加载到错误实现
检查 Thread Context ClassLoader 的值与 META-INF/services 冲突情况。

10.小结

小结:双亲委派是 JVM 提供的一种保守默认策略,目的是保证核心类库优先加载并尽量避免命名冲突与类型不一致。工程中常见的破坏方式包括覆盖 loadClass、SPI 引发的向下查找,以及容器或热部署带来的动态加载。设计时应明确类加载器边界并管理好生命周期与引用清理。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐