在Java的双亲委派模型(Parent Delegation Model)中,类加载器的加载顺序是从子类加载器向父类加载器逐级委派。通常情况下,父类加载器不会再次加载已经被子类加载器加载过的类,因为类加载器的加载机制是基于类的全限定名(Fully Qualified Name)和类加载器的组合来唯一标识一个类的。
以下是详细说明:
1. 类加载器的缓存机制
每个类加载器(包括父类加载器)都会维护一个缓存,用于存储已经加载过的类。当类加载器收到加载请求时,会首先检查缓存中是否已经加载过该类。如果已经加载过,则直接返回缓存的类,而不会重新加载。
- 子类加载器加载的类:子类加载器加载的类会存储在子类加载器的缓存中,父类加载器无法访问子类加载器的缓存。
- 父类加载器加载的类:父类加载器加载的类会存储在父类加载器的缓存中,子类加载器可以通过双亲委派机制访问父类加载器的缓存。
因此,如果子类加载器已经加载了某个类,父类加载器不会再次加载该类,因为父类加载器无法感知子类加载器的缓存。
2. 类的唯一性
在JVM中,类的唯一性是由 类的全限定名 + 类加载器 共同决定的。即使两个类加载器加载了同一个类的字节码,JVM也会将它们视为不同的类。
- 如果子类加载器加载了一个类,父类加载器再次尝试加载同一个类,JVM会认为这是两个不同的类(因为类加载器不同)。
- 这可能导致 类冲突 或 类型转换异常,因为JVM认为这两个类是独立的。
3. 打破双亲委派模型的情况
在某些场景下,如果打破了双亲委派模型(例如重写 loadClass
方法),子类加载器可能会直接加载类而不委派给父类加载器。此时,父类加载器仍然可以尝试加载同一个类,但会导致以下问题:
- 重复加载:父类加载器和子类加载器各自加载同一个类,导致JVM中存在多个相同类的不同实例。
- 类冲突:如果这两个类的实例需要交互(例如类型转换),JVM会抛出
ClassCastException
,因为它们是不同的类。
4. 示例代码
以下是一个简单的示例,展示子类加载器和父类加载器加载同一个类的情况:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// 自定义类加载器
ClassLoader customClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 直接加载类,打破双亲委派
if ("com.example.MyClass".equals(name)) {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
}
return super.loadClass(name);
}
private byte[] loadClassData(String name) {
// 从自定义路径加载类字节码
// ...
}
};
// 使用自定义类加载器加载类
Class<?> clazz1 = customClassLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded by custom class loader: " + clazz1.getClassLoader());
// 使用系统类加载器(父类加载器)加载同一个类
Class<?> clazz2 = ClassLoader.getSystemClassLoader().loadClass("com.example.MyClass");
System.out.println("Class loaded by system class loader: " + clazz2.getClassLoader());
// 检查两个类是否相同
System.out.println("Are the classes the same? " + (clazz1 == clazz2));
}
}
class MyClass {
public static void useCustomLoadedClass(Class<?> clazz) {
System.out.println("Using custom loaded class from system loaded class");
}
public static void useSystemLoadedClass(Class<?> clazz) {
System.out.println("Using system loaded class from custom loaded class");
}
}
输出结果:
Class loaded by custom class loader: com.example.CustomClassLoader@1b6d3586
Class loaded by system class loader: sun.misc.Launcher$AppClassLoader@18b4aac2
Are the classes the same? false
从输出可以看出:
- 自定义类加载器和系统类加载器分别加载了同一个类。
- JVM认为这两个类是不同的,因为它们是由不同的类加载器加载的。
5. 使用场景与注意事项
- 避免重复加载:在设计自定义类加载器时,应尽量避免打破双亲委派模型,除非有明确的需求(如热部署、模块化等)。
- 类隔离:在某些场景下(如OSGi、Tomcat),可以通过自定义类加载器实现类的隔离,确保不同模块或应用加载的类不会冲突。
- 类冲突问题:如果父类加载器和子类加载器加载了同一个类,可能会导致类冲突或类型转换异常,需要谨慎处理。
总结
- 父类加载器不会再次加载已经被子类加载器加载过的类,因为类加载器的缓存机制和类的唯一性规则。
- 如果打破双亲委派模型,父类加载器和子类加载器可能会分别加载同一个类,导致JVM中存在多个相同类的不同实例。
- 在实际开发中,应谨慎设计类加载器,避免类冲突和重复加载问题。