类加载器的双亲委派机制

类加载器

JVM(java虚拟机)有3个子系统:类加载器子系统、执行引擎子系统、垃圾收集子系统。

类加载器就是JVM的一个子系统,类加载器用于加载已存在的class文件到内存中,以供JVM后续运行时使用。

自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构(当然JVM支持自定义类加载器打破双亲委派机制,这种情况在本文有具体的示例代码),尽管这套架构在Java模块化系统出现后有了一些调整变动,但其主体架构依然未改变,本文内容针对JDK8及之前版本的Java来介绍什么是三层类加载器及双亲委派机制。

三层类加载器分别为:启动类加载器(Bootstrap Classloader)、扩展类加载器(Launcher$ExtClassLoader)、应用程序类加载器(Launcher$AppClassLoader)。

● 启动类加载器(Bootstrap Classloader):这个类加载器负责加载<JAVA_HOME>\lib目录,或被-Xbootclasspath参数所指定的路径中存放的,且是JVM所能识别的类库(如rt.jar,tools.jar)加载到JVM的内存中。

● 扩展类加载器(Launcher$ExtClassLoader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或被java.ext.dirs系统变量所指定的路径中的所有的类库。根据“扩展类加载器”这个名称,就可推断出这是一种Java系统类库的拓展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以拓展JavaSE的功能,在JDK9后,这种拓展机制被模块化带来的天然的扩展能力所取代。由于拓展类加载器是由Java代码实现的,开发者可以直接在程序中使用拓展类加载器来加载Class文件。

● 应用程序类加载器(Launcher$AppClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。它负责加载用户类路径(ClassPath)上的所有类库,开发者同样可以在代码中使用这个类加载器。如果应用程序中没有自定义自己的类加载器,这个就是程序中默认的类加载器。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

注意:启动类加载器(Bootstrap Classloader)使用C++语言实现,是JVM自身的一部分;扩展类加载器(Launcher$ExtClassLoader)和应用程序类加载器(Launcher$AppClassLoader)由Java语言实现,这些类加载器独立存在于JVM之外,并且全部继承抽象类java.lang.ClassLoader。启动类加载器无法被Java程序直接引用,如果由启动类加载器加载的类,调用getClassLoader()方法返回的是null(如下所示源码)。开发者无法引用启动类加载器,却可以使用扩展类加载器和应用程序类加载器来加载Class文件。

    /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     */
    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

双亲委派机制

图:类加载器的双亲委派机制

根据上述对类加载器的描述,我们可能会想象这些类加载器的加载流程:各个类加载器只需负责好自己的一亩三分地,自己把指定目录下的class加载完毕就完事了——事实上在“通常”情况下并非如此,如上图所示,类加载器在收到加载请求后,先找父加载器进行加载,如果父加载器能够完成加载则返回父加载器所加载好的类,否则再由自己加载,这种加载机制被成为“双亲委派机制”。(“双亲”:因为JVM的默认类加载器为应用程序类加载器,所以该类加载器往上有两个“亲人”:(爸爸)拓展类加载器、(爷爷)启动类加载器。“委派”:我们可以理解为应用程序类加载器在收到类加载请求时先委派给它的爸爸加载,爸爸搞不定再委派给它的爷爷。爸爸爷爷都搞不定,那只能自己加载了。)

双亲委派机制要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。(不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。)

第一次听说双亲委派机制时,感觉这个名词十分的高大上,但实际上其原理如上所述特别简单,其在源码中的实现也同样非常简单。以下是双亲委派机制在源码中的实现(java.lang.ClassLoader#loadClass(java.lang.String, boolean)):

/**
 * Loads the class with the specified <a href="#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) <tt>loadClass</tt>} method
 *   on the parent class loader.  If the parent is <tt>null</tt> the class
 *   loader built-in to 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
 * <tt>resolve</tt> flag is true, this method will then invoke the {@link
 * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
 *
 * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
 * #findClass(String)}, rather than this method.  </p>
 */
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;
    }
}

读者可能注意到,我在描述双亲委派机制时,用了双引号强调“通常”这个词,这是因为双亲委派机制并非一个强制性约束的机制,而是Java设计者们推荐给开发者的类加载器的实现方式(我们从上面源码中loadClass()方法中的一段注释“Subclasses of ClassLoader are encouraged to override findClass(String), rather than this method.”可以看出这一点);当然这也意味着开发者也可以不通过双亲委派机制来加载类。

我们知道,对于任一一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JVM中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。我们可以举个例子:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要它们的类加载器不同,那么这两个类就必定不相等。(这里所说的相等,包括由等号(“==”)判断,及代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法所判断的两个类是否相等的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。)

使用双亲委派机制一个显而易见的好处就是Java中的类随着它的加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派到处于机制最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有使用双亲委派机制,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为将无从保证,应用程序将会变得一片混乱。如果读者有兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java类,将会发现它可以正常编译,但永远无法被加载运行。(即使自定义了自己的类加载器,强行用defineClass方法去加载一个以"java.lang"开头的类也不会成功。如果读者尝试这样做的话,将会收到一个由JVM内部抛出的“java.lang.SecurityException: Prohibited package name: java.lang”异常。)

打破双亲委派机制

上文提到过双亲委派机制并非一个强制性约束的机制,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,双亲委派机制主要出现过3次较大规模“被打破”的情况。

第一次:双亲委派机制的第一次被打破其实发生在双亲委派机制出现之前——即JDK1.2面世以前的“远古”时代。在JDK1.2之前,用户自定义的类加载器都是重写java.lang.ClassLoader的loadClass方法进行类加载的——这显然没有遵循双亲委派机制。JDK1.2面世后,Java设计者在loadClass方法中实现了双亲委派机制,然后在java.lang.ClassLoader中添加了一个protected修饰的findClass方法,并鼓励开发者去重写这个方法,这样自定义类加载器在加载类时,调用loadClass方法就遵循了双亲委派机制(先调用父类java.lang.ClassLoader的loadClass方法委派“双亲”加载,加载失败后再到重写的findClass方法中加载)。

第二次:双亲委派机制的第二次被打破是由这个机制本身的缺陷导致的。由于双亲委派机制的工作模式是单向的,即自定义类加载器->AppClassLoader->ExtClassLoader->Bootstrap Classloader,只能从左向右请求右边的上层类加载器。在绝大多数情况下,这种单向的委派加载机制是没有问题的。但是如果出现了这样一种情况:Bootstrap Classloader需要加载只有子类加载器才能加载到的类,该如何是好呢?这种情况的一个典型例子就是JNDI服务,JNDI的代码在JDK1.3时被加入到了rt.jar中,由Bootstrap Classloader加载,但是,JNDI需要调用其实现接口SPI的代码,而SPI的代码是被部署在应用程序的ClassPath路径下的。

为了解决这个问题,Java设计团队引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread中的getContextClassLoader()方法和setContextClassLoader(ClassLoader cl)方法来获取和进行设置。如果没有设置,线程上下文类加载器默认就是AppClassLoader。

有了线程上下文类加载器,Bootstrap Classloader在加载JNDI代码后,需要调用SPI代码时,就可以通过线程上下文类加载器去加载SPI的类库了。这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打破了双亲委派机制的层次结构来逆向使用类加载器,已经违背了双亲委派机制的一般性原则,但也是无可奈何的事情。

第三次:双亲委派机制的第三次被打破是由于“热部署”所导致的。热部署最常见的例子是代码热替换(Hot Swap)、模块热部署(Hot Deployment)。比如在应用程序启动后,我们写了一段新的代码,我们要部署新写的代码,怎么办呢?如果没有热部署技术,只能重启整个应用程序;有了热部署后,就不需要重启应用了。这里拿很早就已实现热部署技术的OSGI框架举例,OSGI实现热部署的关键是它自定义的类加载机制。OSGI中的每个模块称为Bundle°@Bundle与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java的Package和Class,每个Bundle都有自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGI中,类加载器不再是双亲委派机制,而是进一步发展更为复杂的网状结构。

在《深入立即Java虚拟机》中,作者在介绍此节内容用了“破坏”一词,这里笔者将它换为“打破”,原因是上面讲的三处案例虽然没有遵守双亲委派机制,但是不遵守并非就一定是不好的,就像原文所说的:只要有明确的目的和充分的理由,突破旧有规则无疑是一种创新。

实例演示双亲委派机制

现在通过代码演示如何自定义类加载器实现双亲委派机制,以及如何绕过这个机制。

1.自定义类加载器,实现双亲委派机制。有两点需要注意:
1)自定义的类加载器,重写ClassLoader的findClass方法
2)加载类时调用loadClass方法(这样就会先调用父类ClassLoader的loadClass方法实现双亲委派机制,如果父类加载器加载失败后,再用我们实现的findClass方法加载)

package myClassLoader;

import java.io.IOException;
import java.io.InputStream;

/**
 * JVM建议我们重写findClass方法,以通过双亲委派机制加载类
 *
 * 自定义类加载器(通过双亲委派机制加载类)
 * 1.编写一个自定义的类加载器,并重写ClassLoader的findClass方法
 * 2.自定义的类加载器调用ClassLoader的loadClass方法加载类
 */
public class ClassLoader_findClass extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name,b,0,b.length); // 将class文件转成字节流,通过读字节流创建类
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader_findClass myClassLoader = new ClassLoader_findClass();

        // 采用ClassLoader的双亲委派机制加载类:先让父类加载器加载,如果父加载器可以加载则返回其加载的类,否则采用我们自定义的findClass方法加载
        Object o = myClassLoader.loadClass("myClassLoader.ClassLoader_findClass").newInstance();
        // 注意:不要直接调用findClass方法,因为会直接调用我们重写的findClass方法,就没有用到双亲委派机制
//        Object o = myClassLoader.findClass("myClassLoader.ClassLoader_findClass").newInstance();

        System.out.println(o.getClass());// class myClassLoader.ClassLoader_findClass

        /*
        虚拟机中只存在一个ClassLoader_findClass类:即由虚拟机默认的类加载器所加载的。执行流程是这样的:当我们执行main方法时(《JVM虚拟机规范》规定了“调用一个
        类型的静态方法的时候”需要进行类的加载。),虚拟机先由默认的类加载器(AppClassLoader)加载此类,然后再执行main方法,在执行到main方法中的加载此类的语句时,
        由于实行的双亲委派机制进行加载,所以直接返回AppClassLoader已经加载好的类。就是说,我们自定义的类加载器加载的类和虚拟机默认的类加载器加载的类是同一个类!
        所以这里打印true
         */
        System.out.println(o.getClass() == ClassLoader_findClass.class); // true 我们自定义的类加载器加载的类和虚拟机默认的类加载器加载的类是同一个类!
        System.out.println(o instanceof ClassLoader_findClass); // true
    }
}

2.自定义类加载器,打破双亲委派机制。有两点需要注意:
1)自定义类加载器,重写ClassLoader的loadClass方法
2)加载类时调用loadClass方法(会直接调用我们重写的loadClass方法,这样就避免了双亲委派机制)

package myClassLoader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 重写loadClass方法打破双亲委派机制
 *
 * 自定义类加载器(不通过双亲委派机制加载类)
 * 1.编写一个自定义的类加载器,并重写ClassLoader的loadClass方法
 * 2.自定义的类加载器调用ClassLoader的loadClass方法加载类
 */
public class ClassLoader_loadClass extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name,b,0,b.length); // 将class文件转成字节流,通过读字节流创建类
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader_loadClass myClassLoader = new ClassLoader_loadClass();

        // 直接调用我们重写的loadClass方法加载类,因此会打破双亲委派机制
        Object o = myClassLoader.loadClass("myClassLoader.ClassLoader_loadClass").newInstance();
        // 注意:直接调用findClass方法报错:ClassNotFoundException,因为我们没有实现该方法
//        Object o = myClassLoader.findClass("myClassLoader.ClassLoader_findClass").newInstance();

        System.out.println(o.getClass());// class myClassLoader.ClassLoader_loadClass

        /*
        “因为Java虚拟机中同时存在了两个ClassLoader_loadClass类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是
        由我们自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类,所以
        做对象所属类型检查时结果自然为false”--《深入理解Java虚拟机》第三版 7.4类加载器
         */
        System.out.println(o.getClass() == ClassLoader_loadClass.class); // false 我们自定义的类加载器加载的类和虚拟机默认的类加载器加载的类是两个不同的类!
        System.out.println(o instanceof ClassLoader_loadClass); // false
    }
}

  

参考:

● 《深入理解Java虚拟机》(第3版)

java findclass_java自定义类加载器(findClass和loadClass这两个方法的差别)

 

posted @ 2021-05-08 20:16  Tom1997  阅读(222)  评论(0编辑  收藏  举报