loadClass执行双亲委派:先查是否已加载,再委托父加载器,父返回null时才调用findClass;不可随意重写loadClass,应只重写findClass并调用defineClass。

loadClass 方法到底做了什么
loadClass 是类加载的入口,但它本身不负责真正读取字节码,而是执行「双亲委派」逻辑:先检查类是否已被加载(findLoadedClass),再委托父加载器尝试加载;父加载器返回 null 时,才调用 findClass 由自己查找并定义类。
常见错误现象:自定义类加载器重写了 findClass 却没调用 super.loadClass,导致 java.lang.String 等核心类被自己的加载器重复加载,触发 LinkageError。
- 不要直接重写
loadClass—— 除非你明确要破坏双亲委派(如 OSGi、热部署) - 正确做法是继承
ClassLoader,只重写findClass,并在其中调用defineClass -
defineClass是受保护方法,它把byte[]转成Class对象,但不触发初始化;若需立即初始化,得额外调用resolveClass
为什么 Bootstrap ClassLoader 显示为 null
当你执行 String.class.getClassLoader(),结果是 null,这不是 bug,而是设计使然:启动类加载器(Bootstrap ClassLoader)由 C++ 实现,不属于 Java 类体系,JVM 不暴露其实例引用。
这直接影响你判断类来源的逻辑。比如想区分一个类是不是 JDK 自带类,不能只靠 getClassLoader() == null,因为某些框架(如 Spring Boot 的 DevTools)会用特殊加载器加载基础类,也可能返回 null 或伪造实例。
立即学习“Java免费学习笔记(深入)”;
- 更稳妥的方式是检查包名前缀:
className.startsWith("java.") || className.startsWith("javax.") - 或用
ClassLoader.getSystemClassLoader().getParent()获取 ExtClassLoader,再逐级向上判断(但 Bootstrap 仍不可达) - 注意:JDK 9+ 引入模块系统后,
getResources("META-INF/MANIFEST.MF")等行为可能因模块封装而失效
自定义类加载器绕过双亲委派的典型场景
双亲委派是默认安全策略,但不是铁律。真实项目中常需打破它,比如:实现插件隔离(各插件加载自己的 log4j 版本)、热更新(替换正在运行的类)、或加载加密/远程字节码。
关键点在于:绕过 ≠ 彻底抛弃。多数场景只需局部绕过,例如仅对特定包路径下的类跳过委派,其余仍走父加载器。
- 重写
loadClass时,先判断类名是否属于「应由我加载」的范围(如com.myapp.plugin.*),是则直接findClass;否则走super.loadClass - 务必避免加载
java.*、javax.*、sun.*等包——否则极易引发NoClassDefFoundError或SecurityException - 类卸载依赖 GC,而 ClassLoader 实例若被静态引用持有(如缓存 map 未清理),会导致整个加载的类无法回收,引发内存泄漏
getResource 和 getResources 容易被忽略的细节
getResource 返回单个 URL,getResources 返回枚举,二者都遵循双亲委派:先查父加载器,再查自身路径。但它们不校验资源是否存在,只返回第一个匹配到的路径(getResource)或全部路径(getResources)。
典型坑:Spring 的 ClassPathResource 默认用当前线程上下文类加载器(Thread.currentThread().getContextClassLoader()),而 Web 容器(如 Tomcat)会切换它——如果你在 Filter 或 Listener 中手动 new 一个类加载器却没设 context,资源就找不到了。
- 调试时可用
classLoader.getResources("application.properties")查看所有匹配路径,确认资源是否真被加载器“看见” - 注意路径分隔符:用
"/",不是"\\"或".";getResource("com/example/Config.class")中的点不会自动转斜杠 - 如果资源在 jar 包里,
URL.getProtocol()通常是"jar",需用URL.openConnection().getInputStream()读取,不能直接new FileInputStream
类加载器真正的复杂性不在机制本身,而在它和线程、模块、容器、安全管理器之间的隐式耦合——一次看似简单的 Class.forName 调用,背后可能横跨三个类加载器、两个线程上下文、一个模块层叠和一套自定义安全管理策略。










