在Java的世界里,类加载器(ClassLoader)扮演着至关重要的角色。它负责将字节码转换为JVM可以执行的Java类。其中,一个重要的概念就是双亲委派模式(Parent Delegation Model)。本文将详细介绍双亲委派模式,包括其定义、工作原理、优点,以及通过Java代码示例进行详细解析。

一、什么是双亲委派模式?

双亲委派模式是Java类加载器在加载类时使用的一种机制。在这种模式下,如果一个类加载器收到了类加载请求,它并不会自己首先去加载,而是把这个请求委派给父类加载器去执行,每一层都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

这种模式的设计初衷是为了保证Java程序运行的稳定性和安全性。由于类加载器的工作是将类的字节码文件转换为JVM可以执行的类,因此如果不同的类加载器加载了同一个类,就可能会出现版本不一致的问题,导致程序运行出错。通过双亲委派模式,可以确保一个类只会被加载一次,从而避免了这种问题。

二、双亲委派模式的工作原理

双亲委派模式的工作原理可以概括为以下几步:

  1. 启动类加载器首先尝试加载类。如果找到了类的字节码文件,就结束加载过程,否则进入下一步。
  2. 父类加载器尝试加载类。如果找到了类的字节码文件,就结束加载过程,否则进入下一步。
  3. 子类加载器尝试加载类。如果找到了类的字节码文件,就结束加载过程,否则抛出ClassNotFoundException。

这个过程可以用以下的Java代码示例来描述:

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先, 检查是否已经加载过这个类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (getParent() != null) {
                        // 如果父类加载器不为null, 则让父类加载器尝试加载这个类
                        c = getParent().loadClass(name, false);
                    } else {
                        // 如果父类加载器为null, 则由启动类加载器尝试加载这个类
                        c = getSystemClassLoader().loadClass(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器和启动类加载器都无法加载这个类, 则由自己加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

在这个示例中,我们可以看到,当一个类加载器收到加载类的请求时,它首先会检查这个类是否已经被加载过。如果这个类已经被加载过,就直接返回这个类;否则,它会尝试让父类加载器去加载这个类。如果父类加载器也无法加载这个类,它才会尝试自己去加载。

这种机制的好处是,它可以确保一个类只会被加载一次,从而避免了版本不一致的问题。同时,由于父类加载器总是先于子类加载器进行类加载,因此可以确保核心Java类库总是由最高优先级的类加载器(即启动类加载器)加载,从而保证了Java程序的稳定性和安全性。

三、双亲委派模式的优点

双亲委派模式的设计思想是为了保证Java核心库的类型安全,所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能会在JVM中存在多个版本的java.lang.Object类,而这些类之间是不兼容的,即便是同名的类,被不同的类加载器加载后,也是完全不同的两个类,这就破坏了双亲委派模式的规则。

此外,双亲委派模式还有以下几个优点:

  1. 避免类的重复加载:Java虚拟机对于每一个加载过的类,都会保存一个对应的元数据。如果不同的类加载器都可以加载同一个类,那么就会产生多个相同的元数据,这不仅会浪费内存空间,还可能会导致程序出错。

  2. 保护Java核心库的安全:通过双亲委派模式,可以确保核心Java类库总是由最高优先级的类加载器(即启动类加载器)加载,这样可以防止恶意代码篡改核心类库,保证了Java程序的安全。

  3. 保持类加载器的独立性:每个类加载器都有自己的命名空间,可以加载自己命名空间内的类,但不能加载其他命名空间的类。这样,即使不同的类加载器加载了同名的类,也不会产生冲突。

四、Java代码示例

下面我们通过一个Java代码示例来进一步理解双亲委派模式。首先,我们定义一个自定义的类加载器:

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading Class '" + name + "'");

        if (name.startsWith("com.myapp")) {
            System.out.println("Loading Class using CustomClassLoader");
            return getClass(name);
        }

        return super.loadClass(name);
    }

    private Class<?> getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', '/') + ".class";
        byte[] b = null;
        try {
            // This loads the byte code data from the file
            b = loadClassFileData(file);
            // defineClass is inherited from the ClassLoader class
            // and converts the byte array into a Class
            Class<?> c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    private byte[] loadClassFileData(String name) throws IOException {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(
                name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        in.readFully(buff);
        in.close();
        return buff;
    }
}

然后,我们可以使用这个自定义的类加载器来加载我们的应用类:

public class MyClass {
    static {
        System.out.println("MyClass has been loaded");
    }

    public MyClass() {
        System.out.println("MyClass has been instantiated");
    }
}

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        ClassLoader myClassLoader = new CustomClassLoader();
        Class<?> myClass = myClassLoader.loadClass("com.myapp.MyClass");
        MyClass object = (MyClass) myClass.newInstance();
    }
}

在这个示例中,我们可以看到双亲委派模式的工作过程:当我们尝试加载MyClass类时,CustomClassLoader首先会检查这个类是否已经被加载过,如果没有被加载过,它会尝试让其父类加载器(系统类加载器)去加载这个类;只有当父类加载器无法加载这个类时,CustomClassLoader才会尝试自己去加载。

五、双亲委派模式的实际应用

在实际的Java开发中,双亲委派模式被广泛应用。例如,Java的Web服务器在加载Web应用时,通常会为每个Web应用创建一个独立的类加载器。当一个Web应用需要加载一个类时,它的类加载器会首先检查这个类是否已经被加载过,如果没有被加载过,它会尝试让父类加载器(通常是系统类加载器)去加载这个类;只有当父类加载器无法加载这个类时,它才会尝试自己去加载。

这样做的好处是,可以避免不同的Web应用之间因为类加载的问题而产生冲突。例如,如果两个Web应用都使用了同一个第三方库,但是版本不同,那么如果不使用双亲委派模式,就可能会因为类加载的问题而导致程序运行出错。

此外,双亲委派模式也被用于Java的热部署技术。热部署技术允许我们在不停止应用的情况下更新应用的代码。这是通过创建一个新的类加载器,使用这个类加载器加载新的类实现的。由于新的类加载器和原来的类加载器是不同的,因此它们加载的类也是不同的,即使类的名称相同。这样,我们就可以在不影响正在运行的应用的情况下,加载新的类。

六、双亲委派模式的深入解析

1. 双亲委派模式与其他模式的对比

双亲委派模式是类加载器的一种设计模式,但并非所有的类加载器都遵循这种模式。例如,OSGi、Jboss、Weblogic等框架的类加载器就不遵循双亲委派模式,而是采用了所谓的“子优先”模式。

在“子优先”模式中,类加载器在接收到类加载请求时,会首先尝试自己去加载这个类,只有在自己无法加载的情况下,才会委托给父类加载器去加载。这种模式的优点是,它可以让每个模块都有自己独立的类加载器,从而实现模块的热部署和卸载。

然而,“子优先”模式也有一些缺点。由于每个模块都有自己的类加载器,因此可能会出现同一个类被多次加载的情况,这不仅会浪费内存,还可能会导致版本不一致的问题。因此,“子优先”模式通常只用在需要支持模块热部署和卸载的场景,而在其他场景下,我们通常还是会选择使用双亲委派模式。

2. 双亲委派模式的实现细节

在Java的类加载器中,双亲委派模式是通过以下几个方法实现的:

  • loadClass(String name, boolean resolve): 这是类加载器的主要方法,它负责处理类加载请求。这个方法首先会检查这个类是否已经被加载过,如果已经被加载过,就直接返回这个类;否则,它会尝试让父类加载器去加载这个类。如果父类加载器无法加载这个类,它才会尝试自己去加载。

  • findLoadedClass(String name): 这个方法用于检查一个类是否已经被加载过。它会在类加载器的内部缓存中查找这个类,如果找到了,就返回这个类;否则,返回null。

  • findClass(String name): 这个方法用于加载一个类。它需要被子类重写,以实现具体的类加载逻辑。

  • defineClass(byte[] b, int off, int len): 这个方法用于将字节码转换为Class对象。它是一个受保护的方法,只能被类加载器内部调用。

  • resolveClass(Class<?> c): 这个方法用于解析一个类。它会确保这个类的所有依赖都已经被加载。

通过这些方法,我们可以实现自己的类加载器,并且可以根据需要选择是否遵循双亲委派模式。

3. 双亲委派模式的应用场景

双亲委派模式主要应用于Java的类加载机制,但其设计思想也可以应用于其他领域。例如,在软件架构设计中,我们可以使用双亲委派模式来组织各个模块的依赖关系。在这种模式下,每个模块都有一个“父模块”,当一个模块需要使用某个功能时,它会首先请求其“父模块”提供这个功能,只有当“父模块”无法提供时,这个模块才会尝试自己实现这个功能。这种模式可以避免功能的重复实现,同时也可以保证模块的独立性。

此外,双亲委派模式也可以用于设计权限管理系统。在这种系统中,每个用户都有一个“父用户”,当一个用户需要执行某个操作时,系统会首先检查其“父用户”是否有这个权限,只有当“父用户”没有这个权限时,系统才会检查这个用户是否有这个权限。这种模式可以有效地管理用户的权限,避免权限的滥用。

七、总结

双亲委派模式是Java类加载器的一个重要特性,它可以确保Java核心库的类型安全,并且可以防止Java类的多次加载。通过理解和掌握双亲委派模式,我们可以更好地理解Java的类加载机制,并且可以在需要的时候,编写自己的类加载器。同时,双亲委派模式也是Java的许多高级特性,如Web应用的隔离、热部署等技术的基础。

👉 💐🌸 公众号请关注 "果酱桑", 一起学习,一起进步! 🌸💐

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐