likes
comments
collection
share

JVM双亲委派模型解析:类加载器如何协同工作?

作者站长头像
站长
· 阅读数 5

引言


  在Java虚拟机(JVM)中,类加载是一个重要的概念,而双亲委派机制是类加载的核心之一。本文将深入研究双亲委派机制,了解它是如何影响Java类加载的。

概述


  类加载是Java虚拟机的一种机制,用于将类的二进制数据读入内存并解析成Class对象。每个Java类都必须由某个类加载器加载到内存中。Java类加载器是通过双亲委派模型实现的,这种模型可以保证同一个类只会被同一个类加载器加载。

  JVM双亲委派机制是Java类加载器的一种工作机制,它的核心思想是:当一个类加载器收到类加载请求时,首先将这个请求委托给父类加载器去完成,依次递归下去,如果父类加载器可以完成该加载任务,就成功返回;只有父类加载器无法完成此加载请求时(找不到此类),子加载器才会尝试自己去加载。这种机制使得Java的核心API能确保在各种环境和应用中保持稳定。

流程

  当一个类加载器收到类加载请求时,会先判断该请求是否合法,如果不合法则直接拒绝;否则,会先将该请求委托给其父类加载器去完成。如果父类加载器无法完成该加载任务,则子类加载器才会尝试自己去加载。

具体来说,双亲委派流程如下:

1)当一个类加载器接收到类加载请求时,首先检查该请求是否合法。

2)如果该请求不合法,则直接拒绝。

3)如果该请求合法,则判断该请求是否需要使用引导类加载器。如果需要使用引导类加载器,则将该请求委托给引导类加载器去完成。

4)如果不需要使用引导类加载器,则将该请求委托给父类加载器去完成。如果父类加载器无法完成该加载任务,则子类加载器才会尝试自己去加载。

5)如果父类加载器可以完成该加载任务,则成功返回。

6)如果所有的父类加载器都无法完成该加载任务,则子类加载器会尝试自己去加载。

图示如下

JVM双亲委派模型解析:类加载器如何协同工作?

工作原理:


  1. 层次结构

    • Java类加载器被组织成一个层次结构,通常包括三个层次:启动类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。这些加载器之间存在父子关系,构成了一颗树状结构。
  2. 加载类请求

    • 当程序需要加载一个类时,当前类加载器首先会检查自己是否已经加载过这个类。如果已加载,它会返回该类的引用。

    • 如果当前类加载器没有加载过这个类,它会将加载请求委派给其父加载器。

  3. 委派给父加载器

    • 父加载器接收到委派请求后,会依照同样的方式检查是否已加载该类。如果已加载,它会返回该类的引用。

    • 如果父加载器也没有加载过该类,它会继续将加载请求委派给其自己的父加载器。这个过程会一直持续,直到达到启动类加载器为止。

  4. 启动类加载器

    • 启动类加载器是位于类加载器层次结构的根部,它通常由JVM的实现提供,负责加载Java核心类库(如java.lang包中的类)。

    • 如果启动类加载器也无法加载该类,它会抛出ClassNotFoundException,指示类找不到。

  5. 加载成功或失败

    • 如果某个父加载器成功加载了类,它将返回该类的引用给子加载器,并加载过程结束。

    • 如果所有父加载器都无法加载该类,当前类加载器会尝试自己加载类。如果加载成功,它会将类引用返回给请求者,加载过程结束。

    • 如果加载失败,当前类加载器会抛出ClassNotFoundException,指示类找不到。

  通过这种双亲委派模型,类加载器可以确保类的唯一性和安全性。核心类库由启动类加载器加载,避免了用户代码替换核心类的风险。同时,类加载器还可以自定义,以满足应用程序的需求,这有助于实现插件化和动态加载类的功能。双亲委派模型是Java类加载机制的一个关键概念,有助于保障Java程序的稳定性和安全性。

优势

  1. 类的唯一性和一致性(Class Uniqueness and Consistency)

    • 双亲委派模型确保了类的唯一性,因为一个类只会被加载一次。如果一个类已经被加载,即使在不同的类加载器命名空间中也不会再次加载,避免了类的多重定义问题。

    • 这有助于维持类加载的一致性,确保在不同的类加载器层次结构中使用的类是同一个版本,避免了类冲突。

  2. 安全性(Security)

    • 双亲委派模型有助于提高Java应用程序的安全性。核心类库由启动类加载器加载,这些类库的完整性受到更严格的控制。

    • 用户自定义类不容易替代核心类库,从而防止潜在的恶意代码注入或不安全的类加载。

  3. 避免重复加载(Avoidance of Redundant Loading)

    • 双亲委派模型避免了重复加载相同的类。当一个类被加载后,它会被缓存,之后的加载请求会直接返回缓存的类引用,提高了性能并节省了内存。
  4. 层次结构(Hierarchy)

    • 双亲委派模型建立了一个层次结构,包括启动类加载器、扩展类加载器和应用程序类加载器。这种层次结构使类加载器的管理更加清晰,有助于隔离不同的类。
  5. 动态加载和模块化(Dynamic Loading and Modularity)

    • 双亲委派模型允许开发人员创建自定义类加载器,这有助于实现动态加载类和模块化的应用程序设计。

    • 插件系统和动态模块化系统可以受益于这种灵活性,可以在运行时加载新的类和模块,而不需要重新启动应用程序。

自定义类加载器

  绕过Java的双亲委派机制通常不是一个推荐的做法,因为双亲委派机制是为了确保类加载的安全性和一致性而设计的。然而,在某些情况下,您可能需要绕过双亲委派机制,例如在实现自定义类加载器时,或者在特殊需求下执行一些高级操作。以下是一些可实现的方法:

  1. 自定义类加载器
// 自定义类加载器
class MyClassLoader extends ClassLoader {
    // 构造函数,需要指定父加载器
    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }

    // 重写类加载方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 在加载类之前可以进行一些自定义的操作
        System.out.println("Loading class: " + name);
        
        // 尝试使用父加载器加载类
        try {
            return super.loadClass(name);
        } catch (ClassNotFoundException e) {
            // 如果父加载器无法加载,自己尝试加载类
            byte[] classData = loadClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                // 使用defineClass方法定义类
                return defineClass(name, classData, 0, classData.length);
            }
        }
    }

    // 模拟从文件或其他来源加载类字节码数据
    private byte[] loadClassData(String className) {
        // 此处可以根据类名加载对应的字节码数据
        // 这里简化为返回一个空的字节数组
        return null;
    }
}

public class ClassLoadingExample {
    public static void main(String[] args) {
        // 创建一个自定义类加载器,以系统类加载器作为父加载器
        MyClassLoader myClassLoader = new MyClassLoader(ClassLoader.getSystemClassLoader());
        
        try {
            // 使用自定义加载器加载一个类
            Class<?> customClass = myClassLoader.loadClass("com.example.CustomClass");
            
            // 创建类的实例并调用方法
            Object instance = customClass.newInstance();
            customClass.getMethod("hello").invoke(instance);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

  在上述示例中,创建了一个自定义的类加载器MyClassLoader,它尝试加载名为"com.example.CustomClass"的类。如果父加载器无法加载,它会从模拟的数据源加载类字节码数据,并使用defineClass方法定义类。

  1. 使用Thread.currentThread().setContextClassLoader()

    • Java提供了Thread类的setContextClassLoader方法,可以设置线程上下文类加载器。这个类加载器将被用于在当前线程中加载类,而不受双亲委派机制的限制。
    ClassLoader customClassLoader = new MyClassLoader();
    Thread.currentThread().setContextClassLoader(customClassLoader);
    // 在这个线程中加载的类将使用自定义类加载器
    
  2. 使用Class.forName()的第二个参数

    • Class.forName()方法允许您通过指定类加载器来加载类,可以绕过双亲委派机制。
    ClassLoader customClassLoader = new MyClassLoader();
    Class<?> myClass = Class.forName("com.example.MyClass", true, customClassLoader);
    

注意:

   绕过双亲委派机制可能会引入类加载的不一致性和安全性问题,因此应慎重使用。通常情况下,最好遵循双亲委派模型,只在确实需要时才考虑绕过它,同时要确保谨慎处理自定义加载器和类加载操作,以防止潜在的问题。

实际应用

双亲委派机制在实际Java开发中有多种应用。其中一些常见的应用包括:

  1. 安全性和防止类冲突:通过双亲委派机制,Java确保核心类库只由启动类加载器加载,从而提高了系统的安全性。这样可以防止用户自定义的类意外地替代核心类库。

  2. 模块化和插件系统:双亲委派模型使得创建模块化和插件化系统更容易。每个模块或插件可以有自己的类加载器,以避免类名冲突,同时还能够访问系统类和其他模块的类。

  3. 动态加载和热部署:双亲委派模型使得动态加载和热部署变得更加可行。自定义类加载器可以加载新的类或更新现有的类,而不必重新启动整个应用程序。

假设有一个简单的插件系统,每个插件都有自己的类加载器,并且可以动态加载和卸载插件:

// 自定义插件类加载器
class PluginClassLoader extends ClassLoader {
    public PluginClassLoader(ClassLoader parent) {
        super(parent);
    }

    public Class<?> loadPluginClass(String className) throws ClassNotFoundException {
        return super.loadClass(className);
    }
}

// 插件接口
interface Plugin {
    void run();
}

// 插件示例
class SamplePlugin implements Plugin {
    @Override
    public void run() {
        System.out.println("SamplePlugin is running.");
    }
}

public class PluginSystem {
    public static void main(String[] args) {
        // 创建一个应用程序类加载器
        ClassLoader appClassLoader = PluginSystem.class.getClassLoader();

        // 创建插件类加载器
        PluginClassLoader pluginClassLoader = new PluginClassLoader(appClassLoader);

        try {
            // 使用插件类加载器加载插件类
            Class<?> pluginClass = pluginClassLoader.loadPluginClass("SamplePlugin");
            
            // 创建插件实例并运行
            Plugin plugin = (Plugin) pluginClass.newInstance();
            plugin.run();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

  在这个示例中,自定义了插件类加载器PluginClassLoader,它继承自应用程序类加载器,并可以加载插件类。这允许在不破坏应用程序类加载器的情况下加载插件。这是一个简单的示例,实际的插件系统会更加复杂,但它演示了双亲委派模型的应用,可以动态加载并运行插件代码。

参考文献

虚拟机规范

深入理解Java虚拟机

openjdk

github.com/openjdk/jdk…

Tomcat 自定义类加载器