核心在于重写loadclass时未调用super.loadclass,导致委派链断裂;正确做法是保留“查缓存→委派父→父失败后findclass”三段式结构,且findclass须校验findloadedclass并确保字节码可信。

排查自定义类加载器因 未正确重写查找类方法 导致双亲委派全面崩溃,核心在于确认类加载行为是否偏离了 JVM 的标准委派链路——重点不是“有没有重写”,而是“怎么重写”破坏了加载顺序、缓存机制或父加载器协作。崩溃常表现为:ClassNotFoundException(连 java.lang.Object 都找不到)、NoClassDefFoundError、LinkageError、静态字段不初始化、甚至线程卡死在 loadClass 同步块里。
检查 loadClass 是否被错误覆盖且绕过父委派
这是最常见也最致命的问题。只要重写了 loadClass(String) 却没按规范调用父类逻辑,双亲委派就实质失效。
- ❌ 错误模式:直接在
loadClass里调用findClass,完全跳过super.loadClass(name) - ❌ 错误模式:做了部分委派但漏掉
findLoadedClass缓存检查,导致重复 defineClass,触发LinkageError - ✅ 正确做法:必须保留三段式结构——查缓存 → 委托父加载器 → 父失败后自己
findClass;哪怕想“自己优先”,也要在 catch 中 fallback 到super.loadClass,不能删掉委派环节
验证 findClass 是否成为唯一入口且逻辑完整
findClass 是自定义加载的真正落点,但它本身不参与委派决策。如果它被误设为公开方法并被外部直调,或内部缺少关键防护,就会让类加载脱离管控。
- 检查是否在
findClass开头调用了findLoadedClass(name)——缺失则同一类可能被多次 define,引发 Metaspace OOM 或类型不兼容 - 确认字节码读取逻辑是否稳定:路径拼接是否适配包名(
name.replace('.', '/'))、文件是否存在、流是否关闭、字节数组是否为空——任一环节异常抛出ClassNotFoundException会中断委派链条 - 避免在
findClass中做耗时操作(如网络请求、解密)且未加超时,这会导致线程长时间阻塞在 synchronized 的loadClass锁内,看起来像“死锁”
确认父加载器是否被显式传入且非 null
自定义加载器若构造时不指定父加载器(例如 super(null) 或无参调用),其 parent 字段就是 null,导致委派链在第一步就断裂。
- 运行时打印
this.getParent(),看是否为null;若是,所有委派都会跳过,连java.lang.*类都得自己加载(显然做不到) - 应显式传入:如
super(ClassLoader.getSystemClassLoader())或继承上下文类加载器Thread.currentThread().getContextClassLoader() - 特别注意:使用
URLClassLoader子类时,其构造函数默认设置父加载器,但若手动重写了构造逻辑却忘了传参,同样会丢失父引用
观察类加载实际路径与类身份一致性
双亲委派“崩溃”的最终表现,是同一个类名由不同加载器加载出多个互不兼容的 Class 对象。可通过日志或调试确认:
- 对关键类(如配置类、Protobuf 生成类、SPI 接口实现类),打印其
clazz.getClassLoader()和clazz.getName(),比对是否本该由系统加载器加载的类,却被你的自定义加载器抢加载了 - 用
jstack查看线程堆栈,若大量线程停在ClassLoader.loadClass的synchronized(getClassLoadingLock(...))处,说明某类正在被并发争抢加载,极可能是findLoadedClass缺失或defineClass被重复调用 - 启用 JVM 参数
-verbose:class,观察类实际由哪个加载器加载——正常情况下,java.*应显示[0x00000007c0000000](Bootstrap),javax.*和用户类应显示具体类加载器哈希值;若看到大量类均由你的加载器加载,委派已名存实亡










