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

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

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

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

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

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() 获取该类加载器的实例。

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

双亲委派模型

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

双亲委派的工作过程

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

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

双亲委派模型的作用

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

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

如下图,在方法区中每个加载器都有自己的一个区域存储自己加载的类类型信息,所以双亲委派模型的重要性体现在此。

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

双亲委派模型的源码实现

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的结构图如下,按照结构图创建项目,把代码复制进去就一定没有错,曾经作者在找类路径这一块卡了一个晚上&middot;·····

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

第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 对象。 所以这也解释了为什么双亲委派模型可以确定系统中只有一个唯一的类。

原文 

https://juejin.im/post/5e8fcc99f265da47e02a7d59

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 浅显易懂的带你掌握双亲委派模型

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址