Java自定义类加载器应重写findClass而非loadClass,以遵循双亲委派机制;findClass负责读取字节码,loadClass默认实现已包含委托逻辑,直接重写易导致ClassNotFoundException或类重复定义。

Java 中自定义类加载器的核心是继承 ClassLoader 类并重写 findClass(String name) 方法,避免直接覆盖 loadClass(String name),以遵循双亲委派机制的规范。
为什么要重写 findClass 而不是 loadClass
loadClass 是类加载的入口,JDK 默认实现已包含双亲委派逻辑(先委托父加载器,失败后再自行加载)。若直接重写它,容易破坏该机制,引发 ClassNotFoundException 或重复定义类等异常。正确做法是:保留 loadClass 的默认行为,只在 findClass 中实现“如何从字节码来源(如文件、网络、加密流)获取 class 字节数组”的逻辑。
- 父类
ClassLoader.loadClass()内部会调用findClass()作为最后一步 -
findClass默认抛出ClassNotFoundException,必须由子类提供具体实现 - 确保自定义逻辑只负责“查找并读取字节码”,不干涉委派流程
一个基础的文件路径类加载器示例
以下是一个从指定目录加载 .class 文件的简单实现:
<font color="#888"><pre class="brush:php;toolbar:false;">public class FileClassLoader extends ClassLoader {
private final String classPath;
public FileClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = loadClassBytes(name);
if (classBytes == null) {
throw new ClassNotFoundException(name);
}
// defineClass 将字节数组转为 Class 对象,且与当前加载器关联
return defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] loadClassBytes(String name) {
String path = classPath + File.separator +
name.replace('.', File.separatorChar) + ".class";
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int b;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
} catch (IOException e) {
return null;
}
}
}
- 构造时传入 classpath 根目录(如
"./myclasses") -
findClass调用loadClassBytes拼路径、读文件;失败返回null,由上层抛异常 -
defineClass是受保护方法,必须由加载器自身调用,确保类与该加载器绑定
绕过双亲委派的典型场景与实现方式
某些场景需打破双亲委派,例如:热部署、同一类不同版本隔离(如插件系统)、自定义加密类加载。这时可重写 loadClass,但须显式控制委派时机:
立即学习“Java免费学习笔记(深入)”;
- 先尝试自己加载(如检查是否属于插件包名),成功则直接返回
- 失败后再调用
super.loadClass(name)委托父加载器 - 注意不要无条件跳过父加载器,否则可能无法加载
java.lang.*等核心类
示例片段:
<font color="#888"><pre class="brush:php;toolbar:false;">@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 优先由本加载器加载 com.example.plugin. 开头的类
if (name.startsWith("com.example.plugin.")) {
Class<?> cls = findLoadedClass(name);
if (cls != null) return cls;
byte[] bytes = loadClassBytes(name);
if (bytes != null) return defineClass(name, bytes, 0, bytes.length);
}
// 其他类走双亲委派
return super.loadClass(name);
}
关键注意事项与常见坑
编写时需特别留意以下几点,否则极易引发 LinkageError、ClassCastException 或内存泄漏:
- 类唯一性由“全限定名 + 加载器实例”共同决定:同一个类被两个不同加载器加载,JVM 视为两个无关类,不能相互强转
- 避免在
findClass中调用Class.forName或其他触发加载的方法,会导致无限递归 - 若加载资源(如配置文件),应使用
this.getResource(...)而非ClassLoader.getSystemResource(...),确保路径相对于当前加载器 - 自定义加载器若持有大对象(如缓存字节数组),需考虑内存泄漏,必要时配合软引用或清理逻辑


















