<p>自定义 ClassLoader 不能简单重写 loadClass(),因其会绕过双亲委派并破坏系统类可见性;正确做法是在 loadClass() 开头判断委托边界,对 java.* 等关键包强制委派父加载器,业务类在 findClass() 中加载。</p>

为什么自定义 ClassLoader 不能简单重写 loadClass()
直接重写 loadClass() 是最常见也最危险的做法——它会绕过双亲委派的默认逻辑,但同时也可能破坏系统类(比如 java.lang.String)的可见性,导致 NoClassDefFoundError 或 LinkageError。
真正要打破双亲委派,核心是「不调用 super.loadClass()」,但必须自己处理委托边界:哪些类该自己加载(如业务 JAR),哪些必须交给父加载器(如所有 java.*、javax.*、sun.*)。
- 漏掉对
java.开头类的强制父委派,JVM 启动后大概率抛SecurityException或直接崩溃 - 把
org.apache.catalina.这类容器内部类也自己加载,会导致 Tomcat 自身组件无法识别自己的类,出现ClassCastException - 推荐在
findClass()中只负责从指定路径读取字节码,而把「是否委派」的判断逻辑放在loadClass()开头
Tomcat 的 WebAppClassLoader 怎么做到应用间隔离
它没彻底抛弃双亲委派,而是「局部反转」:对 /WEB-INF/classes 和 /WEB-INF/lib/ 下的类,优先自己找;但遇到 java.*、javax.*、org.w3c.* 等明确白名单包,立刻向上委派;对其他类,先尝试自己加载,失败后再委派给父类加载器(即 SharedClassLoader)。
这种策略让每个 Web 应用能拥有独立版本的 commons-lang3,又不会各自加载一份 java.util.ArrayList。
- Tomcat 通过
delegate配置项控制默认行为:false表示「先子后父」(默认),true则退化为标准双亲委派 -
WebAppClassLoader会缓存已加载类,但不会缓存失败记录——所以反复尝试加载不存在的类会造成重复 I/O - 它重写了
getResource()和getResources(),确保Thread.currentThread().getContextClassLoader()查资源时也走隔离路径
ClassLoader.defineClass() 报 ClassFormatError 的真实原因
这不是字节码真错了,而是当前 ClassLoader 没被允许定义这个类——JVM 在 defineClass() 时会校验:如果类名以 java. 开头,且当前加载器不是 BootstrapClassLoader,就直接拒绝。
另一个隐蔽原因是「同一类名被不同加载器多次 define」:JVM 不允许同一个 ClassLoader 实例重复 define 同一名字的类,哪怕字节码完全一样,也会报 LinkageError: duplicate class definition。
- 务必在调用
defineClass()前检查findLoadedClass(className)是否已存在 - 不要把原始字节码缓存成静态变量后反复传给
defineClass()——修改字节码时若没重置常量池索引,也会触发格式错误 - 用
javap -v对比报错类和正常类的Constant pool大小,常能发现魔数或版本号被意外改写
热替换场景下,为什么新 ClassLoader 必须丢弃旧实例
类加载器本身是 GC 可回收对象,但只要它加载过的任何类还被引用(比如某个单例持有旧类的实例),整个加载器及其所有加载的类都会被钉在内存里,形成 ClassLoader 泄漏。
Tomcat 在 reload 应用时,会显式调用 WebAppClassLoader.stop(),清理线程上下文、关闭 URLConnection、清空资源缓存,并把自身设为 null —— 这不是为了“优雅”,而是为了让 GC 能真正回收那几 MB 的元空间。
- 忘记清理
ThreadLocal是最常见泄漏源:比如日志框架用ThreadLocal<Map>缓存 MDC,而 key 是旧类加载器下的类 - 使用
Instrumentation.redefineClasses()替代类加载器重建,可避免泄漏,但仅限于方法体变更,不能增减字段或改变签名 - JDK 9+ 的
ModuleLayer提供了更干净的隔离机制,但 Tomcat 10.x 仍未默认启用,需手动配置模块路径
双亲委派不是铁律,但打破它的代价藏在类生命周期的每个角落——尤其是你没显式关掉的那个线程池,或者忘了反注册的那个 JDBC 驱动。










