双亲委派模型

61 阅读9分钟

什么是双亲委派模型

双亲委派模型(Parent Delegation Model)是Java类加载器(ClassLoader)的一种工作机制。它是Java虚拟机(JVM)用来管理类加载的一种层次化结构。

在Java中,每个类加载器都有一个父类加载器,除了顶层的引导类加载器(Bootstrap Class Loader)外。当一个类加载器需要加载某个类时,它首先会委派给其父类加载器来尝试加载。只有在父类加载器无法加载该类时,才会由当前类加载器自己来加载。

以下是双亲委派模型的工作过程:

  1. 当应用程序启动时,JVM会创建一个顶层的引导类加载器(Bootstrap Class Loader)。这个类加载器是由JVM自身实现的,它负责加载Java的核心类库(如java.lang包)和其他基础类。
  2. 每个Java类加载器都有一个父类加载器,包括系统类加载器(System Class Loader)和扩展类加载器(Extension Class Loader)。
  3. 当一个类加载器需要加载某个类时,它会先委派给父类加载器来尝试加载。父类加载器会依次递归委派给其父类加载器,直到达到顶层的引导类加载器。
  4. 如果父类加载器能够成功加载该类,那么加载过程结束,该类就可以被使用。如果父类加载器无法加载该类,那么子类加载器会尝试自己加载该类。
  5. 子类加载器在尝试加载类时,首先检查自己的加载路径,如果找到该类,则加载并返回。如果子类加载器仍然无法找到该类,则会将加载请求委派给父类加载器。
  6. 这个委派过程会一直持续下去,直到类被加载或者所有的父类加载器都无法加载该类。如果所有的父类加载器都无法加载该类,那么子类加载器会抛出ClassNotFoundException。

这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。

为什么需要双亲委派模型

  1. 避免类的重复加载:在Java应用程序中,可能会存在多个不同的类加载器,它们可能会尝试加载同一个类。通过双亲委派模型,每个类加载器在加载类之前都会先委派给父类加载器,因此可以避免重复加载同一个类。这样可以节省内存,并确保在不同的类加载器之间共享已加载的类。
  2. 确保类的一致性:双亲委派模型保证了类的加载顺序是自底向上的,即从引导类加载器开始逐级向上加载。这样可以确保核心类库由引导类加载器加载,避免了应用程序中的类和核心类库发生版本冲突的问题。
  3. 提高安全性:通过双亲委派模型,可以限制对核心类库的访问。由于核心类库由引导类加载器加载,自定义的类加载器无法加载核心类库中的类。这可以防止恶意代码通过自定义类加载器加载和篡改核心类库的行为。
  4. 简化类加载器的实现:双亲委派模型使得类加载器的实现更加简洁和清晰。每个类加载器只需实现加载自己的类和委派给父类加载器的逻辑,而无需关心其他类加载器的细节。这样可以减少类加载器的实现复杂度

破坏双亲委派模型

尽管双亲委派模型在保护类加载的一致性和安全性方面有很多优势,但有些情况下可能需要破坏双亲委派模型。以下是一些破坏双亲委派模型的例子:

  1. 自定义类加载器:通过自定义类加载器,可以破坏双亲委派模型。自定义类加载器可以重写类加载的逻辑,绕过父类加载器的委派机制,直接加载特定的类。这在某些特殊场景下可能是必要的,例如需要从非标准位置加载类或实现特定的类加载策略。

  2. SPI(Service Provider Interface)机制:SPI机制是Java提供的一种扩展机制,允许通过在类路径中配置特定的实现来实现某个接口。SPI机制可以在运行时动态加载实现类,而不受双亲委派模型的限制。通过SPI机制,应用程序可以加载自定义的实现类,而不仅限于双亲委派模型中已加载的类。

  3. Tomcat:Tomcat使用的类加载机制与标准的双亲委派模型略有不同,以支持应用程序的模块化和隔离。在Tomcat中,每个Web应用程序都被隔离到自己的类加载器中,这样可以实现应用程序之间的隔离。每个Web应用程序的类加载器会首先尝试加载Web应用程序内部的类和依赖,如果找不到,则委派给上层的Shared类加载器,然后再委派给Catalina类加载器,最后委派给Bootstrap类加载器。

    这种类加载机制使得每个Web应用程序都可以有自己的类和依赖,而不会相互干扰。这种模块化的类加载机制在一定程度上破坏了传统的双亲委派模型,因为每个Web应用程序都有自己的类加载器,并且在加载类时没有完全依赖于父类加载器。

    需要注意的是,Tomcat的类加载机制并非完全破坏了双亲委派模型,而是在其基础上进行了扩展和调整,以满足Web应用程序的隔离和模块化需求。这种类加载机制在某些情况下可能会引入一些类加载的冲突和问题,需要开发人员在使用Tomcat时注意处理。

  4. Java9模块化:Java 9引入的模块化系统(Module System)在某种程度上可以说是破坏了传统的双亲委派模型。Java 9模块化系统的目标是提供更好的代码隔离和模块化管理,以解决传统Java应用程序面临的复杂性和可维护性问题。

    在传统的双亲委派模型中,类加载器在加载类时会按照一定的层次结构进行委派,确保类的一致性和安全性。但是,这种模型对于大型应用程序或复杂的依赖关系可能不够灵活和精确。Java 9的模块化系统通过引入模块的概念,提供了更细粒度的代码隔离和依赖管理。

    在Java 9的模块化系统中,每个模块都有自己的模块描述文件(module-info.java),其中定义了模块的名称、依赖关系和对外暴露的接口。模块之间的依赖关系是显式声明的,而不是通过类加载器的委派关系来决定。

    这种模块化系统允许开发人员将代码组织成更小、更独立的模块,每个模块只导出必要的接口给其他模块使用,而隐藏内部的实现细节。模块之间的依赖关系是明确的,编译器和运行时环境可以根据模块描述文件进行静态检查和验证,以确保模块之间的依赖关系是正确和可靠的。

    这种模块化系统在一定程度上破坏了传统的双亲委派模型,因为模块之间的依赖关系不再完全依赖于类加载器的委派机制,而是由模块描述文件中的声明来决定。这为开发人员提供了更多的灵活性和精确控制,使得应用程序的组织和维护更加可靠和可扩展。

    需要注意的是,Java 9的模块化系统仍然保留了类加载器的概念,并且仍然使用了类加载器来加载和连接模块。但是,模块化系统通过引入模块的概念,对类加载的过程进行了扩展和调整,以更好地支持模块化开发和应用程序的隔离性。

自定义classloader

public class CustomClassLoader extends ClassLoader {  
    private String baseDir; // 自定义类加载器的基础路径  
  
    public CustomClassLoader(String baseDir) {  
        this.baseDir = baseDir;  
    }  
  
    @Override  
    protected Class<?> findClass(String className) throws ClassNotFoundException {  
        try {  
            // 根据类名和基础路径构造类文件的路径  
            String filePath = baseDir + File.separator + className.replace('.', File.separatorChar) + ".class";  
            // 读取类文件的字节码数据  
            byte[] classData = loadClassData(filePath);  
            // 使用defineClass方法将字节码数据转换为Class对象  
            return defineClass(className, classData, 0, classData.length);  
        } catch (IOException e) {  
            throw new ClassNotFoundException("Failed to load class " + className, e);  
    }  
}  
  
    private byte[] loadClassData(String filePath) throws IOException {  
        // 使用输入流读取类文件的字节码数据  
        try (InputStream inputStream = new FileInputStream(filePath);  
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {  
            byte[] buffer = new byte[1024];  
            int length;  
            while ((length = inputStream.read(buffer)) != -1) {  
                outputStream.write(buffer, 0, length);  
            }  
            return outputStream.toByteArray();  
        }  
    }  
}

使用自定义类加载器时,可以按照以下步骤加载类:

public class TestClassLoad {  
    public static void main(String[] args) throws ClassNotFoundException {  
        // 创建自定义类加载器实例,指定基础路径  
        CustomClassLoader classLoader = new CustomClassLoader("/项目路径/target/classes");  
  
        // 使用自定义类加载器加载指定类  
        Class<?> clazz = classLoader.findClass("com.scoop.loader.Test");  
  
        // 使用加载的类进行操作  
        System.out.println("clazz name: " + clazz.getName());  
        System.out.println("clazz classLoader: " + clazz.getClassLoader());  
        ClassLoader classLoader1 = classLoader;  
        while (null != classLoader1.getParent()) {  
            System.out.println(classLoader1.getParent());  
            classLoader1 = classLoader1.getParent();  
        }  
  
    }  
}

打印结果为:

 clazz name: com.scoop.loader.Test
 clazz classLoader: com.scoop.loader.CustomClassLoader@6193b845
 sun.misc.Launcher$AppClassLoader@18b4aac2
 sun.misc.Launcher$ExtClassLoader@c4437c4

bootstrap ClassLoader 无法直接输出的原因是,它是Java虚拟机内置的类加载器,通常用来加载Java核心类库,而它本身并没有对外公开的toString()方法或相关的接口方法来获取其详细信息。

bootstrap ClassLoader是C++代码实现的一部分,并且通常在Java代码中无法直接访问和操作。它负责加载Java平台的核心类库,如java.lang.Objectjava.lang.String等。由于其实现是由Java虚拟机实现的底层部分,它的加载过程和细节对于普通Java开发者来说是不可见的。