双亲委派模型是java类加载器之间建立的一种层级协作机制,核心规则是当一个类加载器收到类加载请求时,会先向上委托给父加载器去处理,只有当父加载器无法完成加载任务时,子加载器才会尝试自己去加载。这种设计确保了java核心库(如java.lang.object)的安全性和唯一性,防止用户自定义的恶意类替换掉系统核心类。实现该模型的关键在于重写loadclass()方法中的逻辑,通过递归调用父加载器的loadclass()来优先查找。

如何手动实现一个自定义类加载器
要实现双亲委派模型,需要继承java.lang.ClassLoader抽象类并重写findClass()方法,同时在loadClass()中保留对父加载器的委托逻辑。具体做法是:在findClass()中读取自定义路径下的字节码文件并调用defineClass()生成Class对象;在loadClass()中先检查缓存,若无则委托父加载器,若父加载器返回null则调用自己的findClass()。这样既遵循了标准的双亲委派流程,又允许在特定条件下由子类加载器介入。

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassData(String name) {
try {
String filePath = classPath + File.separator + name.replace('.', File.separatorChar) + ".class";
InputStream is = new FileInputStream(filePath);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int len = 0;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
// 假设 D:/classes 目录下有一个 com.example.Test 类
MyClassLoader loader = new MyClassLoader("D:/classes");
try {
Class> clazz = loader.loadClass("com.example.Test");
System.out.println("Loaded by: " + clazz.getClassLoader());
Object instance = clazz.getDeclaredConstructor().newInstance();
System.out.println("Instance created: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行上述代码,如果D:/classes/com/example/Test.class存在且编译正确,控制台将输出该类是由MyClassLoader加载的实例信息。若该类位于rt.jar中,由于父加载器(Bootstrap或Ext)能加载,MyClassLoader不会介入,从而保证了核心类的安全。
破坏双亲委派模型的典型场景是什么
破坏双亲委派模型通常发生在需要让父加载器加载由子加载器定义的类,或者需要在运行时动态加载特定业务逻辑的场景。最典型的例子是JDBC API的实现,因为java.sql.Driver接口位于rt.jar中由Bootstrap类加载器加载,但具体的驱动实现(如MySQL Driver)由应用类加载器加载。为了避免循环依赖和类可见性问题,JDBC使用了线程上下文类加载器(Thread.currentThread().getContextClassLoader())来反向获取类。此外,热部署框架(如Tomcat、OSGi)也通过隔离的类加载器层级来支持同一类名的多版本共存,这本质上也是对标准双亲委派模型的扩展或破坏。

import java.sql.DriverManager;
import java.sql.Connection;
public class JdbcExample {
public static void main(String[] args) {
try {
// JDBC 4.0 之后自动加载驱动,内部使用 Thread Context ClassLoader
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/test", "user", "password"
);
System.out.println("Connection established: " + conn);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在上述JDBC示例中,DriverManager位于核心库中,它无法直接引用第三方驱动的类。因此,它在内部调用Thread.currentThread().getContextClassLoader()来获取当前线程的类加载器(通常是应用类加载器),从而加载具体的驱动类。这种“逆向”委托打破了严格的自下而上委派,实现了SPI(Service Provider Interface)机制。
如果两个不同类加载器加载了同名类会发生什么
如果两个不同的类加载器分别加载了同名同包的类,它们在JVM中被视为完全不同的两个类,即使它们的字节码内容完全一致。这是因为类的唯一性由全限定名和加载它的ClassLoaders共同决定。如果在代码中进行类型转换或比较,会抛出ClassCastException。例如,在一个Web容器(如Tomcat)中,每个Web应用都有独立的WebAppClassLoader,如果两个应用都加载了com.example.Util,它们互不可见,也无法互相强制转换。这种隔离性是Web容器实现应用隔离的基础。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











