浅显易懂的带你掌握双亲委派模型

三种类加载器

启动类加载器(Bootstrap ClassLoader)

负责加载<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径存放的,可以被虚拟机所识别的类库加载到虚拟机的内存中,这个类加载器的底层是由C++实现的,是虚拟机当中的一部分,其它类加载器都是由Java实现的,独立于虚拟机之外,所有继承自java.lang.ClassLoader抽象类。java

在启动类加载器执行时,会加载一个很重要的类:sun.misc.Launcher,这个类里面含有两个静态内部类:app

  1. ExtClassLoader扩展类加载器
  2. AppClassLoader应用程序加载器

在Launcher类加载完成之后会对该类进行初始化,在初始化过程当中会建立两个类加载器的实例,源码以下所示:ide

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
}
复制代码

扩展类加载器(Extension ClassLoader)

负责加载<JAVA_HOME>\lib\ext目录下,或者被java.ext.dirs系统变量所指定的路径中全部的类库。源码分析

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

也被称为“系统类加载器”,负责加载用户类路径(java -classpath)上的全部类库。若是没有自定义类加载器,那么这个加载器就是默认的类加载器。能够经过ClassLoadergetSystemClassLoader()获取该类加载器的实例。post

注意:每种类加载器加载的类信息都会存放在方法区的不一样区域上,因此不一样的类加载器若是加载相同的一个类字节码文件,在虚拟机看来生成的两个类对象是不相同的!若是有两个相同的类User,经过Application ClassLoader加载和Extension ClassLoader加载出来的两个User类对象,是不相同的。测试

双亲委派模型

双亲委派的工做过程

一个类加载器收到类加载的请求时,它不会立刻加载该类,而是把这个请求委托给父加载器去完成,每个层次的类加载器都是如此,所以全部的类加载请求都必须先经过启动类加载器尝试加载,只有当父加载器没法加载这个类时,才会把加载请求传递给它的子加载器去尝试加载,流程以下:this

双亲委派模型的做用

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的加载器一块儿具有了一种带有优先级的层次关系。例如java.lang.Object存放在rt.jar当中,不管哪一层的类加载器须要加载Object类,最终都是委派到启动类加载器进行加载,因此能够保证Object类在程序的各类类加载器环境中是同一个类spa

不一样的类加载器加载同一个类会致使出现相同的字节码文件产生不一样的Class实例信息,即每一个加载器加载同一个字节码文件会保存在方法区的不一样位置,因此若是不用双亲委派模型,若是用户本身写了一个java.lang.Object类,并放在程序的classpath当中,那么系统中会出现多个版本的Object类,那么程序就会一片混乱,使用Object类时不知道该哪一个加载器加载的Class实例。.net

以下图,在方法区中每一个加载器都有本身的一个区域存储本身加载的类类型信息,因此双亲委派模型的重要性体如今此。3d

双亲委派模型的源码实现

protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先检查类是否已经被加载过了
        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异常,说明父加载器没法加载这个类
            }

            if (c == null) {
                //若是没有加载这个类,按顺序调用findClass方法去尝试加载这个类
                long t1 = System.nanoTime();
                c = findClass(name);

                //记录这个类的装入信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
复制代码

双亲委派模型的特色

  1. 父加载器的方法区内存储的类信息对子加载器是可见的,由于子加载器收到类加载请求后会先委派给父加载器,若是父加载器中已经加载了这个类,会直接返回这个类的信息给子加载器,就不用继续加载了。
  2. 双亲委派模型下每一个类都是惟一的,只会由一个类加载器加载。

违背双亲委派的自定义类加载器

咱们能够继承ClassLoader类,覆盖loadClass()而且在代码中不委派给父加载器,而且覆盖findClass()方法定义本身的类加载规则。

咱们定义与系统类库相同的一个类sun.applet.Main,并用自定义的类加载器进行类加载,将其与JVM默认加载的sun.applet.Main做类型判断,判断两个类是否相同。

测试Demo的结构图以下,按照结构图建立项目,把代码复制进去就必定没有错,曾经做者在找类路径这一块卡了一个晚上······

第y一步,自定义sun.applet.Main

package sun.applet;

/** * @author Zeng * @date 2020/4/10 8:22 */
public class Main {
    static {
        System.out.println("customized sun.applet.Main constructed");
    }
    public static void main(String[] args) {
        System.out.println("recognized as sun.applet.Main in jdk," +
                " and there isn't any main method");
    }
}

复制代码

第二步,自定义类加载器UnDelegationClassLoader

package sun.applet;

import java.io.*;

/** * @author Zeng * @date 2020/4/10 8:01 */
public class UnDelegationClassLoader extends ClassLoader {

    private String classpath;

    public UnDelegationClassLoader(String classpath) {
        super(null);
        this.classpath = classpath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        InputStream is = null;
        try {
            String classFilePath = this.classpath + name.replace(".", "/") + ".class";
            is = new FileInputStream(classFilePath);
            byte[] buf = new byte[is.available()];
            is.read(buf);
            return defineClass(name, buf, 0, buf.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    throw new IOError(e);
                }
            }
        }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> clz = findLoadedClass(name);
        if (clz != null) {
            return clz;
        }
        // jdk 目前对"java."开头的包增长了权限保护,这些包咱们仍然交给 jdk 加载
        if (name.startsWith("java.")) {
            return ClassLoader.getSystemClassLoader().loadClass(name);
        }
        return findClass(name);
    }
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
        sun.applet.Main obj1 = new sun.applet.Main();
        UnDelegationClassLoader unDelegationClassLoader = new UnDelegationClassLoader("./out/production/classloading/");
        System.out.println(unDelegationClassLoader.findClassPath());
        String name = "sun.applet.Main";
        Class<?> clz = unDelegationClassLoader.loadClass(name);
        Object obj2 = clz.newInstance();
        System.out.println("obj1 class: "+obj1.getClass());
        System.out.println("obj2 class: "+obj2.getClass());
        System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
        System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
        System.out.println("obj1 == obj2" + obj1 == obj2);
    }
}
复制代码

输出结果以下

能够看到Java类库中Main的实例obj1和自定义的obj2实例的类对象都是sun.applet.Main,可是因为类加载器不一样,因此obj2与Java类库中的Main对象是不相同的。因此若是不进行双亲委派,若是第三方依赖须要调用sun.applet.Main,就会变得糊涂,不知道该调用哪个Main

自定义类加载器的正确姿式

自定义类加载器DelegationClassLoader,并委托给父加载器AppClassLoader

package sun.applet;

import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;

/** * @author Zeng * @date 2020/4/10 7:23 */
public class DelgationClassLoader extends ClassLoader {

    private String classpath;

    public DelgationClassLoader(String classpath, ClassLoader parent) {
        super(parent);
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        InputStream is = null;
        try {
            String fileClasspath = this.classpath + name.replace(".", "\\") + ".class";
            is = new FileInputStream(fileClasspath);
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        }catch (Exception e){
            throw new ClassNotFoundException(name);
        }finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    throw new IOError(e);
                }
            }
        }

    }
}

复制代码

第二步,测试加载重名类sun.applet.Main,因为篇幅问题,只贴出核心方法

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, FileNotFoundException {
        sun.applet.Main obj1 = new sun.applet.Main();
        DelgationClassLoader unDelegationClassLoader = new DelgationClassLoader("./out/production/classloading/", ClassLoader.getSystemClassLoader());
        String name = "sun.applet.Main";
        Class<?> clz = unDelegationClassLoader.loadClass(name);
        Object obj2 = clz.newInstance();
        System.out.println("obj1 class: "+obj1.getClass());
        System.out.println("obj2 class: "+obj2.getClass());
        System.out.println("obj1 classloader: "+obj1.getClass().getClassLoader());
        System.out.println("obj2 classloader: "+obj2.getClass().getClassLoader());
        System.out.println("obj1 instanceof sun.applet.Main: " + (obj1 instanceof sun.applet.Main));
        System.out.println("obj2 instanceof sun.applet.Main: " + (obj2 instanceof sun.applet.Main));
    }
复制代码

输出结果以下图所示:

能够看到不管是Java类库中的sun.applet.Main仍是自定义的sun.applet.Main,JVM都交给了启动类加载器去加载它们,因此在第三方依赖但愿调用sun.applet.Main时,它会很是清楚,只能调用这一个Class对象。因此这也解释了为何双亲委派模型能够肯定系统中只有一个惟一的类。

总结:

这篇文章主要讲解了三种系统默认拥有的类加载器什么是双亲委派模型、双亲委派模型的工做流程、源码分析以及它的特色,使用实际案例演示若是违背双亲委派模型会带来什么后果,反映出双亲委派模型的好处。固然,双亲委派模型不是一种强制约束,它只是虚拟机的其中一种实现,如Tomcat、JDBC等技术就破坏了双亲委派模型,有兴趣的读者能够去深刻了解,做者因为能力有限、时间有限,没法讲解更多更详细深刻的知识点。若是这篇文章对你有小小的帮助,你的点赞是对我最大的鼓励和支持!感谢你的阅读!

巨人的肩膀:

《深刻理解Java虚拟机》第三版

juejin.im/post/5e479c…

blog.csdn.net/lengxiao199…

相关文章
相关标签/搜索