类加载器间是委托链而非继承关系,通过构造参数传递父加载器引用实现双亲委派;自定义加载器默认父为AppClassLoader,Bootstrap无父且由JVM用C/C++实现。

类加载器不是继承关系,而是委托链
很多人误以为 ClassLoader 的父子关系是 Java 类继承(extends),其实不是:它只是构造时传入一个父加载器引用,用于实现双亲委派逻辑。你自定义类加载器时若没显式指定 parent,默认会拿到系统类加载器(AppClassLoader)作为父级。
- 引导类加载器(
Bootstrap ClassLoader)没有父加载器,且不是 Java 实现的——它是 JVM 用 C/C++ 写的,所以ClassLoader.getSystemClassLoader().getParent()返回null - 扩展类加载器(
ExtClassLoader)的父加载器是null,但它内部会主动调用Bootstrap去加载;系统类加载器(AppClassLoader)的父加载器才是ExtClassLoader - 双亲委派不是强制协议,而是
loadClass方法的默认实现逻辑:先调parent.loadClass(name),失败才调自己的findClass(name)
什么时候必须重写 findClass,而不是 loadClass
如果你要从非标准路径(比如数据库、HTTP、加密 ZIP)加载类字节码,**只应重写 findClass**。直接重写 loadClass 会绕过双亲委派,极易引发 ClassNotFoundException 或 LinkageError(比如两个不同加载器都加载了 java.util.ArrayList,它们的类型就不兼容)。
-
findClass负责“找字节码 + defineClass”,不涉及委托逻辑,安全可控 -
defineClass是受保护方法,必须由类加载器自身调用(不能跨加载器调用),否则抛SecurityException - 错误示范:
new MyClassLoader().loadClass("com.example.Foo")若在loadClass里直接读文件并defineClass,就破坏了委派,可能让Foo无法访问它依赖的java.lang.String(因 String 被 Bootstrap 加载,而 Foo 被你的加载器加载,二者类空间隔离)
Class.forName 和 ClassLoader.loadClass 的关键区别
两者都会触发类加载,但初始化行为不同:Class.forName(String) 默认会执行类的 (即运行静态块和静态变量赋值),而 ClassLoader.loadClass(String) **只加载、不初始化**。
- 典型陷阱:用
loadClass加载 JDBC 驱动类(如"com.mysql.cj.jdbc.Driver"),但驱动注册逻辑写在静态块里 → 不触发注册 →DriverManager.getConnection找不到驱动 - 正确做法:用
Class.forName("com.mysql.cj.jdbc.Driver"),或显式传参loadClass(name, true)(第二个参数为true表示初始化) - 性能提示:如果只是做类型检查(比如判断某个类是否存在),用
loadClass更轻量;如果后续要 newInstance 或反射调用静态方法,则必须确保已初始化
常见类加载失败的三个真实原因
遇到 ClassNotFoundException 或 NoClassDefFoundError,别急着查 classpath,先看加载器视角是否一致。
立即学习“Java免费学习笔记(深入)”;
-
NoClassDefFoundError往往是「类曾成功加载过,但在初始化阶段失败(比如静态块抛异常)」,之后再次引用该类时抛出——此时类已被标记为“初始化失败”,JVM 拒绝重试 - Web 应用中,
WEB-INF/lib下两个 JAR 含同名类(如不同版本的slf4j-api),而应用类加载器按顺序扫描 jar,后加载的会覆盖前一个,但若先加载的已参与初始化,就可能引发IncompatibleClassChangeError - OSGi 或模块化(Java 9+)环境下,类可见性受模块声明(
requires)、导出(exports)约束,即使类在 classpath 中,也可能因模块未导出而报IllegalAccessError










