开发者

Java类加载策略之双亲委派机制全面分析讲解

目录
  • 前言
  • 一、什么是双亲委派机制
  • 二、类加载器与层级关系
  • 三、双亲委派机制作用及如何破环机制
    • 为什么需要双亲委派
    • 双亲委派机制的优缺点
    • 如何打破这个机制
    • 有哪些工具选择了破坏机制
  • 四、总结

    前言

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

    一、什么是双亲委派机制

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

    二、类加载器与层级关系

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

    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,会委托启动类加载器完成加载;
    • 若父类加载器无法完成类的加载,当前类加载器才会去尝试加载该类。

    双亲委派机制的优缺点

    优点:

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

    缺点:

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

    如何打破这个机制

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

    想要破坏这种机制,那么就需要自定义一个类加载器,继承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编程客栈 从指定的路径加载类,如果无法加载就委托给父类加载器。这样,我们就实现了打破双亲委派机制的自定义类加载器。

    以下是代码的详细解析:

    • 自定义类加载器 CustomClassLoader 继承 Java ClassLoader 类。
    • 在类加载器的构造方法中设置自定义类加载器的类路径 classesPath 和父加载器 parent
    • 重写 loadClass 方法。首先检查已加载的类,如果已加载则返回。否则尝试用自定义的方法在 classesPath 中加载类。如果自定义加载方法找不到类,则委托给父类加载器。
    • 实现名为 findClassInPath 的自定义加载方法。这个方法使用类名 classNameclpythonassesPath 指定的目录下查找对应的 .class 文件,然后将文件内容读取为字节数组并调用 defineClass 方法,将其转换为 Java 类的 Class 对象。如果类不存在或出现其他错误,会抛出 ClassNotFoundException 异常。
    • 在 main 方法中,创建一个 CustomClassLoader 类的实例。将类的加载任务交给自定义类加载器,指定加载路径和要加载的类。
    • 使用自定义类加载器加载目标类,创建类的实例,并调用指定方法。

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

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

    • 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应用类型安全,同时避免类加载冲突。在某些特定场景下,我们可以通过自定义类加载器对类加载策略进行调整,以满足应用特性和性能需求。

    以上就是Java双亲委派机制全面分析讲解的详细内容,更多关于Java双亲委派机制的资料请关注编程客栈(www.cppwww.devze.comcns.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜