【Java】深入了解双亲委派机制(常说的类加载机制)

前言:

ava虚拟机(JVM)的类加载机制是Java应用中不可或缺的一部分。本文将详细介绍JVM的双亲委派机制,并阐述各关键点。

一、什么是双亲委派机制?

双亲委派机制(Parent-Delegate Model)是Java类加载器中采用的一种类加载策略。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。

二、类加载器与层级关系

Java中的类加载器主要有如下三种:

  • 启动类加载器(Bootstrap ClassLoader): 负责加载 %JAVA_HOME%/jre/lib 目录下的核心Java类库如 rt.jar、charsets.jar 等。

  • 扩展类加载器(Extension ClassLoader): 负责加载 %JAVA_HOME%/jre/lib/ext 目录下的扩展类库。

  • 应用类加载器(Application ClassLoader): 负责加载用户类路径(ClassPath)下的应用程序类。

这三种类加载器之间存在父子层级关系。启动类加载器是最高级别的加载器,没有父加载器;扩展类加载器的父加载器是启动类加载器;应用类加载器的父加载器是扩展类加载器。

  除了以上三个内置类加载器,用户还可以通过继承 java.lang.ClassLoader 类自定义类加载器,根据实际需求处理类加载请求。

三:双亲委派机制作用及如何破环机制

通过上述两块内容,我们对双亲委派机制、加载流程及层级有了一些了解,这时我们不妨抛出几个疑问。

  • 为什么需要双亲委派
  • 双亲委派机制有哪些优缺点
  • 如何打破这个机制
  • 有哪些工具选择了破坏机制。

1. 为什么需要双亲委派

  1. 通过双亲委派机制,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。

  2. 通过双亲委派机制,可以保证安全性。因为BootstrapClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.String,那么这个类是不会被随意替换的。

那么,就可以避免有人自定义一个有破坏功能的java.lang.String被加载。这样可以有效的防止核心Java API被篡改。

这是在JDK1.8的java.lang.ClassLoader类中的源码,这个方法就是用于加载指定的类。
实现双亲委派机制 的代码也都集中在这个方法之中:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

通过以上代码得出结论:

  • 当类加载器接收到类加载的请求时,首先检查该类是否已经被当前类加载器加载;
  • 若该类未被加载过,当前类加载器会将加载请求委托给父类加载器去完成;
  • 若当前类加载器的父类加载器为null,会委托启动类加载器完成加载;
  • 若父类加载器无法完成类的加载,当前类加载器才会去尝试加载该类。

2. 双亲委派机制的优缺点

优点:

  1. 避免重复加载:由于类加载器直接从父类加载器那里加载类,避免了类的重复加载。
  2. 提高安全性:通过双亲委派模型,Java 标准库中的核心类库(如 java.lang.*)由启动类加载器加载,这样能保证这些核心类库不会被恶意代码篡改或替换,从而提高程序的安全性。
  3. 保持类加载的一致性:这种方式确保了同一个类的加载由同一个类加载器完成,从而在运行时保证了类型的唯一性和相同性。这也有助于减轻类加载器在处理相互关联的类时的复杂性。

缺点:

  1. 灵活性降低:由于类加载的过程需要不断地委托给父类加载器,这种机制可能导致实际应用中类加载的灵活性降低。
  2. 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。

3. 如何打破这个机制

既然上述文章中我们已经知道了双亲委派的实现方式,那么如何打破这个机制呢。

想要破坏这种机制,那么就需要自定义一个类加载器,继承ClassLoader类重写其中的loadClass方法,使其不进行双亲委派即可。
写个示例

import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class CustomClassLoader extends ClassLoader {
    
    // 自定义类加载器必须提供一个加载类文件的位置
    private String classesPath;

    public CustomClassLoader(String classesPath, ClassLoader parent) {
        super(parent);
        this.classesPath = classesPath;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        //首先,检查已加载的类
        Class<?> loadedClass = findLoadedClass(name);

        if (loadedClass == null) {
            // 如果已加载类中没有该类, 尝试用自定义的方法加载
            try {
                loadedClass = findClassInPath(name);
            } catch (ClassNotFoundException e) {
                // 如果自定义加载方法找不到类,则委托给父类加载器
                loadedClass = super.loadClass(name, resolve);
            }
        }

        if (resolve) {
            resolveClass(loadedClass);
        }
        
        return loadedClass;
    }

    private Class<?> findClassInPath(String className) throws ClassNotFoundException {
        try {
            String filePath = className.replace('.', '/') + ".class";
            byte[] classBytes = Files.readAllBytes(Paths.get(classesPath, filePath));

            return defineClass(className, classBytes, 0, classBytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("Class not found in classes path: " + className, e);
        }
    }

    public static void main(String[] args) throws Exception {
        String pathToClasses = "/path/to/your/classes";
        String className = "com.example.SampleClass";
        String methodName = "sampleMethod";

        // 创建自定义类加载器实例,将类的加载权交给它
        CustomClassLoader customClassLoader = new CustomClassLoader(pathToClasses, CustomClassLoader.class.getClassLoader());

        // 使用自定义类加载器加载类
        Class<?> customClass = customClassLoader.loadClass(className);

        // 创建类的实例并调用方法
        Object obj = customClass.newInstance();
        Method method = customClass.getDeclaredMethod(methodName);
        method.setAccessible(true);
        method.invoke(obj);
    }
}

上面的示例代码中,我们重写了 loadClass 方法,先尝试通过 findClassInPath 从指定的路径加载类,如果无法加载就委托给父类加载器。这样,我们就实现了打破双亲委派机制的自定义类加载器。

以下是代码的详细解析:

  1. 自定义类加载器 CustomClassLoader 继承 Java ClassLoader 类。

  2. 在类加载器的构造方法中设置自定义类加载器的类路径 classesPath 和父加载器 parent

  3. 重写 loadClass 方法。首先检查已加载的类,如果已加载则返回。否则尝试用自定义的方法在 classesPath 中加载类。如果自定义加载方法找不到类,则委托给父类加载器。

  4. 实现名为 findClassInPath 的自定义加载方法。这个方法使用类名 classNameclassesPath 指定的目录下查找对应的 .class 文件,然后将文件内容读取为字节数组并调用 defineClass 方法,将其转换为 Java 类的 Class 对象。如果类不存在或出现其他错误,会抛出 ClassNotFoundException 异常。

  5. 在 main 方法中,创建一个 CustomClassLoader 类的实例。将类的加载任务交给自定义类加载器,指定加载路径和要加载的类。

  6. 使用自定义类加载器加载目标类,创建类的实例,并调用指定方法。

4. 有哪些工具选择了破坏机制。

既然在上文中,我们已经清楚怎么打破双亲机制,那么有哪些工具选择了破坏机制呢?为什么?

  • OSGi(Open Service Gateway Initiative):OSGi 是一个模块化系统和服务平台,提供了一个强大的类加载器模型。在 OSGi 中,每个模块都有一个独立的类加载器,可以按需加载来自不同模块的类。这有助于解决 JAR 地狱问题,提高模块化和动态更新能力。

  • Tomcat Web容器:Tomcat 的 Web 应用类加载器可以加载 Web 应用程序中的本地类库,从而使得每个 Web 应用程序可以使用各自的版本的类库。这些 Web 应用的类加载器都是${tomcat-home}/lib 中类库的子类加载器。

  • Java Agent: Java Agent 是一种基于 Java Instrumentation API 的技术,它可以在运行时修改已加载的类的字节码,从而实现类的热替换、AOP(面向切面编程)等功能。这种技术在诸如热部署、性能监控和分布式追踪等场景中有广泛应用。

  • JDK 中的 URLClassLoader:JDK 自带的 URLClassLoader 可以用来加载指定 URL 路径下的类。实际上,它实现了一种子类优先的策略,先尝试加载自身路径下的类,再委托给父类加载器,从而打破了双亲委派机制。

这些工具和技术之所以要打破双亲委派机制,主要是出于以下原因:

  • 实现模块化和动态更新:例如 OSGi,通过独立的类加载器实现不同模块间解耦,并支持模块的动态卸载和更新。

  • 解决类库版本冲突(JAR地狱问题):在复杂系统中,不同模块可能依赖不同版本的类库。为避免版本冲突,可使用独立的类加载器,使它们分别加载各自的类库版本。

  • 运行时修改类:Java Agent 可以在运行时修改类字节码,从而支持热替换、AOP 和性能监控等功能。

  • 支持 Web 应用程序的独立部署和更新:例如 Tomcat,可以为每个 Web 应用程序分配一个独立的类加载器,实现各自部署与更新。

需要注意的是,打破双亲委派机制可能会带来类加载冲突、安全性和性能等问题,因此在实际应用中要谨慎使用。

四:总结

本文介绍了JVM的双亲委派机制,包括概念、类加载器层级关系、双亲委派流程及实例分析等方面的内容。双亲委派机制可以确保Java应用类型安全,同时避免类加载冲突。在某些特定场景下,我们可以通过自定义类加载器对类加载策略进行调整,以满足应用特性和性能需求。

本文到此就结束了,多谢观看。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月15日
下一篇 2023年12月15日

相关推荐