类加载器的双亲委派机制

1、类加载器

  什么是类加载器?上篇博客我们介绍类加载过程中的第一个阶段——加载,作用是“通过一个类的全限定名来获取描述此类的二进制流”,那么这个加载过程就是由类加载器来完成的。

  从Java虚拟机的角度出发,只存在两种不同的类加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另一种是所有其它的类加载器,这些类加载器都是由Java语言实现的。但是从Java开发人员的角度来看,类加载器可以细分为如下四种:

①、启动类加载器(Bootstrap ClassLoader)

  负责将存放在 <JAVA_HOME>/lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机按照文件名识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
  
启动类加载器无法被Java程序直接引用。

  JDK 中的源码类大都是由启动类加载器加载,比如前面说的 java.lang.String,java.util.List等,需要注意的是,启动类 main Class 也是由启动类加载器加载。

②、扩展类加载器(Extension ClassLoader)

   这个类加载器由 sun.misc.Launcher$ExtClassLoader 实现,负责加载<JAVA_HOME>/lib/ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。

  开发者可以直接使用扩展类加载器。

③、应用程序类加载器(Application ClassLoader)

  由 sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是 ClassLoader.getSystemClassLoader() 方法的返回值,所以一般也称它为系统类加载器。

  它负责加载用户类路径ClassPath上所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

   通常项目中自定义的类,都会放在类路径下,由应用程序类加载器加载。

④、自定义类加载器(User ClassLoader)

   这是由用户自己定义的类加载器,一般情况下我们不会自定义类加载器,但有些特殊情况,比如JDBC能够通过连接各种不同的数据库就是自定义类加载器来实现的,具体用处会在后文详细介绍。

2 类加载器的双亲委派机制

除了根类加载器之外,其他的类加载器都需要有自己的父加载器。从JDK1.2开始,类的加载过程采用双亲委派机制,这种机制能够很好的保护java程序的安全。除了虚拟机自带的根类加载器之外,其余的类加载器都有唯一的父加载器。比如,如果需要classLoader加载一个类时,该classLoader先委托自己的父加载器先去加载这个类,若父加载器能够加载,则由父加载器加载,否则才有classLoader自己加载这个类。即每个类加载器都很懒,加载类时都先让父加载器去尝试加载,一直到根类加载器,加载不到时自己才去加载。真正加载类的加载器我们叫做启动类加载器。注意,双亲委派机制的父子关系并非面向对象程序设计中的继承关系,而是通过使用组合模式来复用父加载器代码,这种机制如下图所示:

public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();

        while (classLoader!=null){
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }

使用双亲委派机制的好处:

1、可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子ClassLoader再加载一次。

2、考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Object,而直接返回已加载过的Object.class,这样便可以防止核心API库被随意篡改。

package java.lang;

/**
 * @author WGR
 * @create 2020/4/26 -- 20:58
 */
public class MyObject {
    //加载该类
    public static void main(String[] args) {
        Class clazz = MyObject.class;
        System.out.println(clazz.getClassLoader());
    }
}

因为java.lang包属于核心包,只能由根类加载器进行加载,而根据类加载的双亲委派机制,根类加载器是加载不到这个MyObject类的(自定义的),所以只能由AppClassLoader进行加载,而这又不是允许的,所以会报出“Prohibited package name: java.lang”(禁止的包名)错误。

ClassLoader

所有的类加载器(除了根类加载器)都必须继承java.lang.ClassLoader。它是一个抽象类,主要的方法如下:

loadClass

在ClassLoader的源码中,有一个方法loadClass(String name,boolean resolve),这里就是双亲委托模式的代码实现。从源码中我们可以观察到它的执行顺序。需要注意的是,只有父类加载器加载不到类时,会调用findClass方法进行类的查找,所以,在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法。

//ClassLoader类的loadClass源码
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;
        }
    }

findClass

在自定义类加载器时,一般我们需要覆盖这个方法,且ClassLoader中给出了一个默认的错误实现。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass

该方法的签名如下。用来将byte字节解析成虚拟机能够识别的Class对象。defineClass()方法通常与findClass()方法一起使用。在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象。

protected final Class<?> defineClass(String name,byte[] b,int off,int len)
                              throws ClassFormatError

resolveClass

连接指定的类。类加载器可以使用此方法来连接类。

URLClassLoader

在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。

构造方法:

public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。

public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。

案例1:加载磁盘上的类

 //加载该类
    public static void main(String[] args) throws Exception {
        File file = new File("d:/");
        URI uri = file.toURI();
        URL url = uri.toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.topcheer.Demo");
        Object obj = aClass.newInstance();
    }

 案例2:加载网络上的类

    public static void main(String[] args) throws Exception{
        URL url = new URL("http://localhost:8080/examples/");
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.topcheer.Demo");
        aClass.newInstance();
    }

posted @ 2020-04-26 21:23  天宇轩-王  阅读(279)  评论(0编辑  收藏  举报