ClassLoader

ClassLoader 源码分析

ClassLoader 解决的问题:把二进制的 class 字节流按需变成 JVM 里的 Class 对象,并且保证核心类库不被冒名顶替。核心思路是双亲委派——加载请求先逐级交给父加载器,全都找不到才轮到自己,而这套委派逻辑用模板方法写死在 loadClass 里,留给子类的扩展点只有 findClass

// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— 双亲委派主线
public abstract class ClassLoader {

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;   // 委派意义上的父, 不是继承; VM 硬编码了它的字段偏移, 新字段只能加在它后面

    // ... 其余字段与构造器省略, 见下一段

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);      // 对外入口默认 resolve=false: 只加载不主动链接
    }

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {  // 并行加载器按类名细分锁, 否则锁整个 loader 实例
            Class<?> c = findLoadedClass(name);     // 1. 先查缓存: 自己作为发起者加载过就直接返回
            if (c == null) {
                long t0 = System.nanoTime();        // 只为下面 PerfCounter 统计委派耗时, 与加载逻辑无关
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);      // 2. 向上委派, 递归直到顶; 注意父加载时不 resolve
                    } else {
                        c = findBootstrapClassOrNull(name);     // 3. parent==null 代表 Bootstrap, 它在 VM 内部没有 Java 对象
                    }
                } catch (ClassNotFoundException e) {
                    // 父加载器找不到是正常流程, 吞掉异常轮到自己; 这就是为什么自定义加载失败的栈里常有一层被吞的 CNFE
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);            // 4. 全链路都没有, 才调子类的 findClass 真正去读字节码
                    // ... PerfCounter 三行统计代码省略
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 唯一推荐的扩展点: 默认实现直接抛 CNFE, 逼着子类去实现"怎么找字节码"
    // 只重写它而不动 loadClass, 委派逻辑就原封不动地保留了
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    static Class<?> findBootstrapClassOrNull(String name) {
        if (!checkName(name)) return null;
        return findBootstrapClass(name);    // 进 VM 查 Bootstrap 加载的类, 查不到返回 null 而非抛异常
    }

    private static native Class<?> findBootstrapClass(String name);

    protected final Class<?> findLoadedClass(String name) {    // final: 缓存查询不许子类篡改
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);      // 查 VM 里"以本 loader 为发起者"的加载记录, 委派给父成功的也算
    }

    private final native Class<?> findLoadedClass0(String name);

    protected final void resolveClass(Class<?> c) {
        if (c == null) {
            throw new NullPointerException();   // 坑: 现在是空操作, 链接由 VM 首次使用时按需完成, "resolve 阶段"不由它触发
        }
    }
}
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— 并行加载锁
public abstract class ClassLoader {

    // 类名 -> 锁对象。VM 同时用这个字段是否为 null 来判断 loader 是否 parallel capable
    private final ConcurrentHashMap<String, Object> parallelLockMap;

    private ClassLoader(Void unused, String name, ClassLoader parent) {
        this.name = name;
        this.parent = parent;
        this.unnamedModule = new Module(this);
        if (ParallelLoaders.isRegistered(this.getClass())) {    // 必须在 static 块里 registerAsParallelCapable, 实例化后再注册无效
            parallelLockMap = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            parallelLockMap = null;     // 未注册: 退化为锁整个 loader 实例
            assertionLock = this;
        }
        this.package2certs = new ConcurrentHashMap<>();
        this.nameAndId = nameAndId(this);
    }

    protected Object getClassLoadingLock(String className) {
        Object lock = this;                     // 默认粗粒度: 整个 loader 一把锁
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);    // 每个类名一把锁, 不同类可并发加载
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
}
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.ClassLoader —— defineClass 链: byte[] 变 Class
public abstract class ClassLoader {

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);    // final: 字节码进 JVM 的口子不许子类绕过安检
    }

    private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        if ((name != null) && name.startsWith("java.")
                && this != getBuiltinPlatformClassLoader()) {   // Java 层就堵死伪造 java.* 的路, invoke 包的防伪检查依赖这条不变式
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) {
            checkCerts(name, pd.getCodeSource());   // 同包必须同签名者, 防止往已签名的包里塞恶意类
        }

        return pd;
    }

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);   // 真正干活在 VM: 解析+校验类文件, 记录 defining loader
        postDefineClass(c, protectionDomain);
        return c;
    }

    static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);
}

原理串讲

以一次典型加载走通全链路:业务代码触发 loader.loadClass("com.foo.Bar"),进入 loadClass(name, false)
第一件事是 getClassLoadingLock(name) 拿锁——如果这个 loader 在 static 块里调过 registerAsParallelCapable(),锁是 parallelLockMap 里按类名分配的独立 Object,不同类名可以并发加载;否则锁就是 loader 自己。
为什么要按类名细分?因为在 OSGi 这类非层次委派环境里,两个 loader 可能互相委派:A 持有自己的实例锁去请求 B,B 同时持有自己的锁反过来请求 A,一把粗锁直接死锁。
类名粒度的锁把”加载 X”和”加载 Y”解耦,环形委派只要不落在同一个类名上就不会互相卡死。

拿到锁后先 findLoadedClass(name),它透过 native 的 findLoadedClass0 查 VM 的记录:只要本 loader 曾作为发起加载器(initiating loader)加载过这个名字,直接返回缓存的 Class,这保证了同一个 loader 对同一个名字永远给出同一个 Class 对象。
缓存未命中才开始委派:parent != null 就递归调 parent.loadClass(name, false),一路爬到 AppClassLoader、PlatformClassLoader;到顶时 parent == null,改调 findBootstrapClassOrNull(name) 进 VM 查 Bootstrap。
为什么 Bootstrap 用 null 表示而不给个对象?因为它是 VM 自身的一部分(HotSpot 里是 C++ 实现),在 Java 堆里根本没有对应实例,null 就是它在 Java 世界的名字——这也是 String.class.getClassLoader() 返回 null 的原因。

整条父链都返回 null 或抛出 ClassNotFoundException(被 catch 静默吞掉,父找不到是正常分支),才轮到 findClass(name)
这里是第二个”为什么这么设计”:委派顺序写死在 loadClass 里,而扩展点单独抽成 findClass,是标准的模板方法模式——JDK 不信任子类会自觉维护委派,干脆让子类根本碰不到委派逻辑就能完成自定义加载。
你写类加载器时重写 findClass,从磁盘/网络/解密后的字节流拿到 byte[],然后调 defineClass

defineClass 先过 preDefineClass 安检:checkName 拦非法类名,接着是那条著名的 java. 前缀检查——普通 loader 想定义 java.lang.Object 直接 SecurityException
这个检查放在 Java 层而 VM 层还有一道(Bootstrap 校验包归属),双保险是因为 java.lang.invoke.MemberName 的防伪逻辑整个建立在”java.* 不可能被冒充”这条不变式上,一旦破了就是任意代码提权。
checkCerts 再保证同一个包里所有类的签名证书一致。安检通过后 defineClass1 进 VM:解析类文件格式、字节码校验、创建 Class 对象,并把本 loader 记为 defining loader。
最后 postDefineClass 补登记包信息和签名者。回到 loadClass 尾部,resolve 为 true 会调 resolveClass(c)——但注意看源码,它现在只剩判空,链接(验证/准备/解析)早已由 VM 推迟到首次使用时按需进行,这个方法名留着只是兼容历史。

设计取舍

  • 双亲委派是约定不是强制:loadClass 只是 protected 而非 final,Tomcat 的 Webapp 加载器、SPI(线程上下文加载器)都合法地打破了它。真正 final 的是 findLoadedClassdefineClass 这两个安全底线。
  • 类的唯一标识是「类名 + defining loader」二元组:同一个 class 文件被两个 loader 各自 define,得到两个互不兼容的 Class,instanceof 为 false、强转抛 ClassCastException——热部署框架的经典坑。
  • 重写 findClass 是”接入”委派,重写 loadClass 是”改写”委派;99% 的需求(自定义字节码来源、加密类)只需要前者。
  • 委派链上父加载器一律传 resolve=false,链接的决定权留给最初的发起者;而如今 resolveClass 本身已是空操作,实际链接时机完全由 VM 掌控。
  • 自定义并行加载器必须在 static 初始化块里调 registerAsParallelCapable(),且要求全部父类也已注册;实例创建之后 parallelLockMap 已定型,再注册无效。

延伸阅读