美文网首页
类加载器与双亲委派机制

类加载器与双亲委派机制

作者: 新生代民工代表 | 来源:发表于2021-08-05 11:08 被阅读0次

    前言

    什么是类加载器?
    类加载器有哪些?
    双亲委派机制是怎么样的?
    什么时候需要打破双亲委派?
    如何打破?

    以上内容网上有大量文章介绍一些基础概念,这篇《类加载器、双亲委派机制与打破(Driver、TCCL、Tomcat等)》《我竟然被“双亲委派”给虐了》有详细介绍,本文着重从源码来讲解下我在理解过程中的疑问

    用途

    先在前面说,了解这个有啥用,我认为有两点:

    • 可以通过网络、数据库、接口等多种形式进行远程class的加载,这个就很有用了,你的代码部署在别人那边,核心class在你这儿
    • 代码加密,你的代码部署给用户,自定义加载器来加载解密等操作
    • 热部署,运行过程中,直接上传class文件,然后自定义加载,不用重启,spring boot那个热部署插件就使到这个
      以上建议还要配合密钥,时间等综合来考虑,不然单一的还是没法保障安全性

    疑问

    1.jvm默认的类加载器:AppClassLoader、ExtClassLoader、Bootstrap ClassLoader;三者是如何在代码里面没有继承关系,是如何进行逐步委托加载的?

    首先他们三不是用extends进行继承操作的,是基于组合进行的松耦合继承,可以看ClassLoader类里面的parent属性,其次rt.jar中sun.misc.Launcher类中有两个静态类AppClassLoaderExtClassLoader,查看Launcher的构造函数如下:

    public Launcher() {
            Launcher.ExtClassLoader var1;
            try {
                //获取ext类加载器,点进getExtClassLoader()方法可以看到加载的范围为
                // String var0 = System.getProperty("java.ext.dirs");
                var1 = Launcher.ExtClassLoader.getExtClassLoader();
            } catch (IOException var10) {
                throw new InternalError("Could not create extension class loader", var10);
            }
    
            try {
                //获取app类加载器,点进getAppClassLoader()方法可以看到final String var1 = System.getProperty("java.class.path");
                //注意此处传入了var1,创建app时,将ext传入,跟进代码可以到顶层抽象类ClassLoader.java,代码见下文
                this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
            } catch (IOException var9) {
                throw new InternalError("Could not create application class loader", var9);
            }
            //注意这个线程上下文类加载器,此加载器可以用户JDBC、spring等打破双亲加载机制,默认的线程上下文加载器 == app加载器
            Thread.currentThread().setContextClassLoader(this.loader);
            String var2 = System.getProperty("java.security.manager");
            if (var2 != null) {
                SecurityManager var3 = null;
                if (!"".equals(var2) && !"default".equals(var2)) {
                    try {
                        var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                    } catch (IllegalAccessException var5) {
                    } catch (InstantiationException var6) {
                    } catch (ClassNotFoundException var7) {
                    } catch (ClassCastException var8) {
                    }
                } else {
                    var3 = new SecurityManager();
                }
    
                if (var3 == null) {
                    throw new InternalError("Could not create SecurityManager: " + var2);
                }
    
                System.setSecurityManager(var3);
            }
    
        }
    
     private ClassLoader(Void unused, ClassLoader parent) {
            //此时this指向app,parent指向ext,且parent为该类的成员变量,这种关系没有使用继承,采用的基于松耦合的组合关系
           // 由于启动类加载器是c++实现的,在java的视角里是没有此对象的,所以为null;换而言之,如果this指向是ext,那么他的parent为null,此处在loadClass方法中有体现,见下文代码
            this.parent = parent;
            if (ParallelLoaders.isRegistered(this.getClass())) {
                parallelLockMap = new ConcurrentHashMap<>();
                package2certs = new ConcurrentHashMap<>();
                domains =
                    Collections.synchronizedSet(new HashSet<ProtectionDomain>());
                assertionLock = new Object();
            } else {
                // no finer-grained lock; lock on the classloader instance
                parallelLockMap = null;
                package2certs = new Hashtable<>();
                domains = new HashSet<>();
                assertionLock = this;
            }
        }
    
      protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
               //看看这个findLoadedClass方法的位置,说明相同的类只会加载一次
               //这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        //父加载器不为空,就一直往上递归,直至交给顶层父类加载器加载,结合上面代码注释可知,从自定义加载器开始,到app、到ext,都会递归往上找parent,直到parent == null
                        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
                        //进行递归退出的处理
                    }
                    //从顶层开始,每一层类加载器加载不到,就会逐步往下找可以加载的类加载器,所以自定义加载器是需要重写这个findClass方法的
                    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;
            }
        }
    

    2.双亲委派模型中的"双亲"如何理解?

    并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器;

    • 父类加载器(parent classloader):它可以替子加载器尝试加载类;
      他是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的,他的parent为null,由上文loadclass代码可知,为空就会调用findBootstrapClassOrNull()
    • 引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。

    3.为什么jdbc驱动加载要打破双亲委派

    因为Driver接口是java.sql包下的,根据规定,这个应该是bootstrap classloader来加载,但是具体的实现是各个厂商来做的,bootstrap classloader无法加载实现类,这个时候就需要子类加载器加载(加载过程是逐步往上递归,父类加载不到逐步由子类来加载,由此时直接交给app来加载不就行了么,返回为null,直接走app classloader 的findclass方法进行加载不就可以么)

    针对上面个问题,重新捋下逻辑
    1.jdbc的加载是由DriverManager来执行,这个类在rt下面,由根加载器来加载
    2.该类有个静态代码块,里面有loadInitialDrivers()方法,然后跟进方法里面可以看到 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    //这儿涉及spi机制来加载厂商的实现
    3.厂商的代码肯定不在rt包里面,目前的加载器是根加载器,无法加载厂商实现,jdk的方案是代码如下:

    public static <S> ServiceLoader<S> load(Class<S> service) {
          ClassLoader cl = Thread.currentThread().getContextClassLoader();
           return ServiceLoader.load(service, cl);
       }
    

    可以看到是在当前根加载器执行加载期间,从当前线程获取到上线文加载器,去加载厂商的实现类

    我的问题来了:这儿如果不用上下文来加载,直接返回加载不到,return null; 然后直接走app classloader 的findclass方法进行加载不就可以么?
    答:被这个问题困扰了几天,想不通,后面猜测如果返回为空,那之前根加载到的DriverManager怎么办,是直接放弃了么,我理解的是整个DriverManager加载和厂商驱动加载是一体的原子性的,必须要在根加载器加载到一半的时候继续去加载厂商的,如果这个时候返回为空,那么后面app是可以加载厂商实现,但是DriverManager怎么办,不加载了么,要加载的话,又是根加载器来加载

    这个理解不知道对不对

    4.为什么tomcat要打破双亲委派模型的?

    从问题1我们知道,类的加载是逐步委托给顶层的类加载器进行加载,加载不到时才会由下面的加载器进行加载;
    假设我们tomcat容器中有三个server,server1的jackson是1.0;server2的jackson是2.0;server3的jackson是3.0;
    三个版本里面的某个类的方法里面的实现可能不一样,但是由于双亲委派机制的存在,都会交由顶层类加载器来加载,如果没有自定义,那么应该是app加载,就会导致该类最终会被覆盖成某个版本;(类在jvm中的唯一性由类加载器名和类全路径决定)
    因为我们的三个server是不同的应用,所以我们需要各个server进行隔离,独自加载,不应该使用双亲委派交由顶层加载

    1. tomcat是如何打破双亲委派模型的?
      首先推荐一篇优秀的文章 [Tomcat 的架构有哪些牛逼之处?](https://mp.weixin.qq.com/s/_bsAOTA10fGDJsz2jCYivg) 从架构、实现等多个维度层面进行分析,文章较难,需要多次阅读

    org.apache.catalina.loader.WebappClassLoader,该类重写了findclass方法

    ublic Class<?> findClass(String name) throws ClassNotFoundException {
        ...
    
        Class<?> clazz = null;
        try {
                //1. 先在 Web 应用目录下查找类
                clazz = findClassInternal(name);
        }  catch (RuntimeException e) {
               throw e;
           }
    
        if (clazz == null) {
        try {
                //2. 如果在本地目录没有找到,交给父加载器去查找
                clazz = super.findClass(name);
        }  catch (RuntimeException e) {
               throw e;
           }
    
        //3. 如果父类也没找到,抛出 ClassNotFoundException
        if (clazz == null) {
            throw new ClassNotFoundException(name);
         }
    
        return clazz;
    }
    

    同样的也重写了loadclass方法

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
        synchronized (getClassLoadingLock(name)) {
    
            Class<?> clazz = null;
    
            //1. 先在本地 cache 查找该类是否已经加载过(主要是tomcat自定义加载器中查找)
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
    
            //2. 从系统类加载器的 cache 中查找是否加载过(从jvm加载的类里面查找)
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
    
            // 3. 尝试用 ExtClassLoader 类加载器类加载(此处是精髓,注意是ext,不是app,基础类和扩展类都是交由双亲来加载,避免了覆盖 JRE 核心类,保证虚拟机的正常运行)
            ClassLoader javaseLoader = getJavaseClassLoader();
            try {
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
    
            // 4. 尝试在本地目录搜索 class 并加载(这儿就是tomcat自定义加载器加载了)
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
    
            // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
           (其他类还有交由app加载)
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
           }
    
        //6. 上述过程都加载失败,抛出异常
        throw new ClassNotFoundException(name);
    }
    
    tomcat加载类关系图.png
    • WebAppClassLoader
      Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context容器负责创建和维护一个 WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。

    • SharedClassLoader
      本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗。
      因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader自己没有加载到某个类,就会委托父加载器 SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

    • CatalinaClassloader
      如何隔离 Tomcat 本身的类和 Web 应用的类?
      要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。
      这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?
      老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader和 SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被 CatalinaClassLoader和 SharedClassLoader使用
      //TODO

    6.类加载成功后会立即调用构造函数进行实例化么

    不会,如果涉及到new等创建对象才会实例化,类加载后是会初始化静态代码块,静态变量等,类的初始化和对象的初始化是两个事情

    7.自定义类加载器是如何保障加载器执行顺序的,即app怎么成为自定义加载器的parent

    ClassLoader类里面默认的parent是app

    protected ClassLoader() {
           this(checkCreateClassLoader(), getSystemClassLoader());
    }
    

    8.当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式

    总结:

    • 参考classloader里面申明 private final ClassLoader parent;然后子类基于组合的集成方式,之后递归双亲委派【从这儿我们可以抄作业,就是我们设计底层框架,借鉴这个模式,从而实现父类调用子类的方
    • tomcat架构运用模板方法模式。分别运用了组合模式、观察者模式、骨架抽象类和模板方法,需要自己去体会如何使用的,使用和不使用的区别

    相关文章

      网友评论

          本文标题:类加载器与双亲委派机制

          本文链接:https://www.haomeiwen.com/subject/aermvltx.html