类加载器调用loadclass()时先查缓存,再委托父加载器,直至bootstrap;全部失败后才调用findclass()加载字节码;自定义类加载器应覆盖findclass()而非loadclass(),避免破坏双亲委派导致linkageerror。

类加载器收到 loadClass() 请求时,到底发生了什么
它不会立刻去磁盘找 .class 文件,而是先问“我爸能不能 load?”——这个“爸”不是继承关系里的父类,而是逻辑上的委托链顶端。启动类加载器(Bootstrap ClassLoader)没实例、不可见,但它是所有委派的终点;扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader)才是 Java 代码里能接触到的两级“中间人”。
真实流程是:loadClass(String name, boolean resolve) 方法内部先查缓存(findLoadedClass(name)),再递归调用父加载器的 loadClass(),直到父为 null 时才 fallback 到 findBootstrapClassOrNull(name);全部失败后,才轮到自己调用 findClass(name) 去真正读字节码。
- 别重写
loadClass():自定义类加载器时,99% 的场景只需覆盖findClass();重写loadClass()会绕过双亲委派,极容易引发LinkageError或核心类被重复加载 -
resolve参数控制是否触发链接阶段(验证、准备、解析),多数情况下保持默认false即可,JVM 在首次主动使用类时自动 resolve - 注意
findClass()必须抛出ClassNotFoundException,不能静默返回null,否则上层委派逻辑会误判为“父已加载成功”
为什么 String.class.getClassLoader() 返回 null
因为 java.lang.String 是由 C++ 实现的 Bootstrap ClassLoader 加载的,而 JVM 规定:任何 Java 语言编写的类加载器都无法引用它,所以返回 null 是设计使然,不是 bug。
这直接导致一个常见陷阱:用 getClassLoader() 判断类来源时,不能靠“是否为 null”来区分“系统类”和“用户类”,得结合包名(如 java.、javax.、sun.)或调用 ClassLoader.getSystemClassLoader() 做基准对比。
立即学习“Java免费学习笔记(深入)”;
- 错误写法:
if (clazz.getClassLoader() == null) { /* 是系统类 */ }—— 不可靠,某些 JDK 内部工具类可能由AppClassLoader加载但包名也带sun. - 正确思路:优先用白名单(如
name.startsWith("java."))+ 类加载器层级判断(clazz.getClassLoader() == ClassLoader.getSystemClassLoader())组合识别 - 调试技巧:运行时加
-verbose:class,看每行输出里 “loaded by” 后面是谁,比猜更准
打破双亲委派不是叛逆,而是有明确场景需求
双亲委派不是铁律,JDK 自己就在破——Thread.getContextClassLoader() 就是典型妥协:当 SPI(如 JDBC 驱动)需要由业务代码提供实现时,必须让子加载器(比如 Web 应用的 WebAppClassLoader)反向提供类给父加载器(AppClassLoader)用,否则 ServiceLoader 找不到驱动类。
自定义类加载器破委派,只应在以下情况考虑:
- 热部署/模块隔离(如 OSGi、Spring Boot DevTools):每个模块用独立加载器,避免类冲突
- 从非标准路径加载(数据库 BLOB、HTTP URL、加密 ZIP):父加载器根本不知道这些源,必须自己
findClass() - 防止类被篡改(如游戏客户端):对字节码解密后再 define,且不走标准 classpath 路径
- 切记:破委派后,
java.*类仍必须由 Bootstrap 加载,否则SecurityException或NoClassDefFoundError立刻出现
ClassLoader.defineClass() 和 findClass() 的分工边界
defineClass() 是 JVM 提供的“最终落锤”方法:它把原始字节码校验后塞进方法区,生成 Class 对象;而 findClass() 是你作为类加载器的“守门员”:只负责定位、读取、可能解密字节码,然后把干净字节交给 defineClass()。
常见翻车点在于混淆二者职责:
- 在
findClass()里直接 new Class?不行,JVM 不允许用户手动构造Class实例 - 跳过
defineClass()自己解析字节码结构?危险,会绕过字节码验证,可能加载非法类导致 JVM 崩溃 - 传给
defineClass()的字节数组被复用或修改?必须确保字节数组不可变,否则后续resolveClass()可能读到脏数据 - 推荐姿势:
protected Class> findClass(String name) { byte[] bytes = loadBytesFromCustomSource(name); return defineClass(name, bytes, 0, bytes.length); }
最易被忽略的是:同一个类加载器多次调用 defineClass() 加载同名类,会直接抛 LinkageError;而 findClass() 没这限制——所以缓存字节码、避免重复 define,是你自己的事。










