背景知识

详细见类加载器 章节

双亲委派模型

双亲委派模型的委派机制约定了类加载器的加载机制。按照双亲委派模型的规则,除了启动加载器之外,程序中每一个类加载器都应该有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader,开发者编写的自定义类加载器的超类就是AppClassLoader.
那么当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,而是将加载的任务委派给他的超类加载器去执行,每一层的类加载器都采用相同的方式,直至派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给他的类时,便会将类的加载任务退回给他的下一级类加载器去执行加载。
使用双亲委派模型的优点就是能够有效地确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载的时候,始终只会加载其中某一个类,如果通过defindClas()方法进行显示加载则JVM会抛出异常。

以下是JDK 中双亲委派的实现

/**
     * Loads the class with the specified <a href="#binary-name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) loadClass} method
     *   on the parent class loader.  If the parent is {@code null} the class
     *   loader built into the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * {@code resolve} flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting {@code Class} object.
     *
     * <p> Subclasses of {@code ClassLoader} are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock getClassLoadingLock} method
     * during the entire class loading process.
     *
     * @param   name
     *          The <a href="#binary-name">binary name</a> of the class
     *
     * @param   resolve
     *          If {@code true} then resolve the class
     *
     * @return  The resulting {@code Class} object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
*/
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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派机制有一定的区别,当缺省的类加载器接收到到一个类的加载任务时,他首先会自行加载,当它加载失败时,才会将类的加载任务委派给他的超类加载器去执行,这也是servlet规范推荐的一种做法。

破坏双亲委派模型

JDk种破坏双亲委派模型

  • JDK1.0
    java.lang.ClassLoader 提供了loadClass() 方法,继承ClassLoader类,重写loadClass()方法

  • SPI
    一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,
    它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。

为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

  • OSGi
    双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
    OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
    在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
    当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
    1)将java.*开头的类委派给父类加载器加载。
    2)否则,将委派列表名单内的类委派给父类加载器加载。
    3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
    4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
    5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
    6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
    7)否则,类加载器失败

  • JKD9模块系统
    模块化加载源码片段

    Class<?> c = findLoadedClass(cn);
          if (c == null) {
             // 找到当前类属于哪个模块
             LoadedModule loadedModule = findLoadedModule(cn);
             if (loadedModule != null) {
                //获取当前模块的类加载器
                BuiltinClassLoader loader = loadedModule.loader();
                //进行类加载
                c = findClassInModuleOrNull(loadedModule, cn);
             } else {
                // 找不到模块信息才会进行双亲委派
             if (parent != null) {
               c = parent.loadClassOrNull(cn);
             }
           }

整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。

经过破坏后的双亲委派模型更加高效,减少了很多类加载器之间不必要的委派操作
JDK9的模块化可以减少Java程序打包的体积,同时拥有更好的隔离线与封装性
每个moudle拥有专属的类加载器,程序在并发性上也会更加出色。