美文网首页
08. 双亲委派模型

08. 双亲委派模型

作者: ZFH__ZJ | 来源:发表于2020-02-19 12:04 被阅读0次

双亲委派模型

在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器

某一个类加载器想要加载一个特定的类,并不是立刻由自己加载,而是把这个加载的动作委托给自己的父亲完成,父亲上面还有父亲,再委托给父亲的父亲完成,一直往上追溯到最顶层的根类加载器,由根类加载器尝试加载需要的.class文件,如果根类加载器加载不成功,则把流程往下返回给扩展类加载器,从上往下一直返回到开始的类加载器,然而在整个过程中,在任何一个类加载器的层次上成功的加载这个类,就宣告这个加载动作是成功的,流程直接返回开始的类加载器

若有一个类加载器能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能够成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器

双亲委派模型的好处

  1. 可以确保Java核心库的类型安全:

    所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载
    到Java虚拟中,如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥作用),借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,它们之间是相互兼容的

  2. 可以确保Java核心类库所提供的类不会被自定义的类所替代

  3. 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际应用。

通过例子来验证双亲委派模型

看上篇文章我们定义的第二个类加载器,代码如下

public class MyTest16_2 extends ClassLoader{

    // 类加载器名字
    private String classLoaderName;

    // 新增字段path 从什么地方加载 绝对路径
    private String path;

    // 新增set方法
    public void setPath(String path) {
        this.path = path;
    }

    // 字节码文件扩展名
    private final String fileExtension = ".class";

    public MyTest16_2(String classLoaderName) {
        // 使用getSystemClassLoader()返回的类加载器,即AppClassLoader作为该类加载器的父类加载器
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyTest16_2(ClassLoader parent, String classLoaderName) {
        // 显式指定该类加载器的父类加载器
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 这两个信息输出 说明findClass()方法被调用
        System.out.println("findClass invoked: " + className);
        System.out.println("class loader name: " + this.classLoaderName);

        byte[] data = this.loadClassData(className);
        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        // windows 系统 替换成\\
        className = className.replace(".", "/");

        try {
            is = new FileInputStream(new File( this.path + className + this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = is.read())){
                baos.write(ch);
            }
            data = baos.toByteArray();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                is.close();
                baos.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    @Override
    public String toString() {
        return "[" + this.classLoaderName + "]";
    }
}

试验1

使用上面的MyTest16_2这个自定义类加载器,我们进行如下代码测试,猜测下输出是什么?

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是classpath
        loader1.setPath("/Users/zj/workspace/java/study/jdk8/target/classes");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object = clazz1.newInstance();
        System.out.println(object);
    }
}

此时,使用的是系统类加载器来加载,使用loader1来加载类,loader1的父加载器是系统类加载器,其委托给系统类加载器去加载,系统类加载器委托扩展类加载器去加载,扩展类委托启动类加载器去加载,启动类加载器发现自己不能加载该类,就让扩展类加载器去加载,扩展类加载器发现自己也不能加载,就让系统类加载器去加载,系统类加载器在classpath找到该类可以加载,就自己加载返回了,所以此时是系统类加载器加载的

试验2

我们修改下代码,并进行如下操作

在桌面递归创建文件夹 mkdir -p ~/Desktop/com/zj/study/jvm/classloader

将MyTest1.class文件 移动到上面创建的文件夹中 cp com/zj/study/jvm/classloader/MyTest1.class ~/Desktop/com/zj/study/jvm/classloader

重新build一下项目

代码如下

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object = clazz1.newInstance();
        System.out.println(object);
    }
}

此时使用的还是系统类加载器,原因和上面那个例子相似.

试验3

还是同样的代码,进行如下操作

将classpath 下的MyTest1.class删除

代码如下

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object = clazz1.newInstance();
        System.out.println(object);
    }
}

此时 使用的是自定义类加载器
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载

试验4

新增了一个自定义类加载器loader2,加载路径和加载的类一致,重新build一下项目

代码如下

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object1 = clazz1.newInstance();
        System.out.println(object1);

        MyTest16_2 loader2 = new MyTest16_2("loader2");
        loader2.setPath("/Users/zj/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz2.hashCode());
        System.out.println("classLoaderName: " + clazz2.getClassLoader());
        Object object2 = clazz1.newInstance();
        System.out.println(object2);
    }
}

此时使用的是系统类加载器,class1 和 class2 是同一个对象
loader1 和 loader2 的父类加载器都是系统类加载器,classpath下有MyTest1.class文件,所以此时系统类加载器此时可以加载MyTest1,
所以loader1 和 loader2 委托系统类加载器加载MyTest1

试验5

同样的代码,将classpath 下的MyTest1.class删除

代码如下

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object1 = clazz1.newInstance();
        System.out.println(object1);

        MyTest16_2 loader2 = new MyTest16_2("loader2");
        loader2.setPath("/Users/zj/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz2.hashCode());
        System.out.println("classLoaderName: " + clazz2.getClassLoader());
        Object object2 = clazz1.newInstance();
        System.out.println(object2);

    }
}

此时loader1 和 loader2 分别加载了Mytest1 两个class1 和 class2 不是同一个对象
loader1 和 loader2 的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader1 和 loader2 有各自的命名空间,命名空间概念后面再说

试验7

将loader2的父加载器改为loader1

将classpath 下的MyTest1.class删除

public class Test {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object1 = clazz1.newInstance();
        System.out.println(object1);

        MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
        loader2.setPath("/Users/zj/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz2.hashCode());
        System.out.println("classLoaderName: " + clazz2.getClassLoader());
        Object object2 = clazz1.newInstance();
        System.out.println(object2);
    }
}

此时使用的loader1 加载类,两个class1 和 class2 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader1 和 loader2 是同一个命名空间

此时同样的代码,重新build一下项目
此时使用的是系统类加载器,两个class1 和 class2 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载

类加载器好像是层次结构,但真实情况是包含关系,子加载器包含父加载器的引用
loader1 和 loader2 虽然都是MyTest16_2的实例,但是loader1可以作为loader2的父加载器

试验8

新增loader3,将classpath 下的MyTest1.class删除

public class Test {
// 
    // 此时若
    // 
    // 
    // 
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object1 = clazz1.newInstance();
        System.out.println(object1);

        MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
        loader2.setPath("/Users/zj/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz2.hashCode());
        System.out.println("classLoaderName: " + clazz2.getClassLoader());
        Object object2 = clazz1.newInstance();
        System.out.println(object2);

        MyTest16_2 loader3 = new MyTest16_2("loader3");
        loader3.setPath("/Users/zj/Desktop/");
        Class<?> clazz3 = loader3.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz3.hashCode());
        System.out.println("classLoaderName: " + clazz3.getClassLoader());
        Object object3 = clazz3.newInstance();
        System.out.println(object3);
    }

}

此时loader1、loader2使用的loader1 加载类,两个class1 和 class2 是同一个对象
loader3 使用loader3加载类,class3 是另外一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader1 和 loader2 是同一个命名空间
loader3的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader3 和 loader1 不是同一个命名空间

同样的代码,重新build一下项目

此时使用的是系统类加载器,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader3同loader1

试验9

将loader3的父加载器改成loader2,此时若将classpath 下的MyTest1.class删除

public class Test {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
        MyTest16_2 loader1 = new MyTest16_2("loader1");
        // 这个路径是新的文件夹
        loader1.setPath("/Users/zj/Desktop/");

        Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz1.hashCode());
        System.out.println("classLoaderName: " + clazz1.getClassLoader());
        Object object1 = clazz1.newInstance();
        System.out.println(object1);

        MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
        loader2.setPath("/Users/zj/Desktop/");
        Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz2.hashCode());
        System.out.println("classLoaderName: " + clazz2.getClassLoader());
        Object object2 = clazz1.newInstance();
        System.out.println(object2);

        MyTest16_2 loader3 = new MyTest16_2(loader2,"loader3");
        loader3.setPath("/Users/zj/Desktop/");
        Class<?> clazz3 = loader3.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz3.hashCode());
        System.out.println("classLoaderName: " + clazz3.getClassLoader());
        Object object3 = clazz3.newInstance();
        System.out.println(object3);
    }
}

此时loader1、loader2,loader3使用的loader1 加载类,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader3的父类加载器是loader2,loader2的父类加载器是loader1,loader1能加载
loader1 loader2 loader3 是同一个命名空间

重新build一下项目
此时使用的是系统类加载器,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader3的父类加载器是loader2,loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载

通过自定义类加载器来进一步理解各个类加载器负责的范围

自定义类加载器MyClassLoader

public class MyClassLoader extends ClassLoader{

    private String classLoaderName;

    private final String fileExtension = ".class";

    private String path;

    public void setPath(String path) {
        this.path = path;
    }

    public MyClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader classLoader, String classLoaderName) {
        super(classLoader);
        this.classLoaderName = classLoaderName;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 这两个信息输出 说明findClass()方法被调用
        System.out.println("findClass invoked: " + className);
        System.out.println("class loader name: " + this.classLoaderName);

        byte[] data = this.loadClassData(className);
        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassData(String className) {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        // windows 系统 替换成\\
        className = className.replace(".", "/");

        try {
            is = new FileInputStream(new File( this.path + className + this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = is.read())){
                baos.write(ch);
            }
            data = baos.toByteArray();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                is.close();
                baos.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    @Override
    public String toString() {
        return "[" + this.classLoaderName + "]";
    }

}

试验1

看如下代码

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader("loader1");
        myClassLoader.setPath("/Users/zj/Desktop/");

        Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz.hashCode());
        System.out.println("class loader: " + clazz.getClassLoader());
    }
}

此时 是由系统类加载器加载的

试验2

把com.zj.study.jvm.classloader.MyTest1 放到 /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes目录

具体操作
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre
sudo mkdir -p classes/com/zj/study/jvm/classloader
cd /Users/zj/workspace/java/study/jdk8/target/classes
sudo cp com/zj/study/jvm/classloader/MyTest1.class /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes/com/zj/study/jvm/classloader

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader("loader1");
        myClassLoader.setPath("/Users/zj/Desktop/");

        Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz.hashCode());
        System.out.println("class loader: " + clazz.getClassLoader());
    }
}

此时是由启动类加载器加载的

修改启动类加载器加载的目录,如
java -Dsun.boot.class.path=. com.zj.study.jvm.classloader.MyTest18
会出现如下异常
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object
在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果设置错误,则运行会出错,提示如下错误信息
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

把/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes目录 com.zj.study.jvm.classloader.MyTest1删除

试验3

把项目打成jar包 放到 /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext看是否由扩展类加载器加载
具体操作
sudo rm -rf /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext

public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader("loader1");
        myClassLoader.setPath("/Users/zj/Desktop/");

        Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
        System.out.println("class: " + clazz.hashCode());
        System.out.println("class loader: " + clazz.getClassLoader());
    }
}

此时是由扩展类加载器加载

扩展类加载器加载的是jar包,单纯的把.class文件放到扩展类加载器负责的目录是不可以的

然后删除
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext
sudo rm -rf jdk8-1.0.jar

相关文章

  • 08. 双亲委派模型

    双亲委派模型 在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有...

  • Tomcat类载入器

    大家都知道,Java的类加载机制是双亲委派模型,那么什么是双亲委派模型呢?我们这里简要的说一下,双亲委派模型...

  • java类加载破坏双亲委派模型

    前面java类加载器与双亲委派模型中提到Java采用个双亲委派的方式来完成类加载,但是双亲委派模型并不是一个强制的...

  • Java 类加载

    双亲委派模型 并非强制 而是推荐 SPI 父类加载器需要子类加载器加载类 打破双亲委派模型 https://www...

  • sandBox源码分析之ClassLoader

    提起classLoader,就不由自主想起了java classLoader的双亲委派模型,那么到底什么是双亲委派...

  • 双亲委派模型

    一、类加载器简介:JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构: Bootstrap ClassL...

  • 双亲委派模型

    一、为什么要用这种模型 保证相同的字节码文件不被重复加载 二、利用双亲委派模型加载类的过程 java.lang.C...

  • 双亲委派模型

    jdk1.2后 虚拟机 的类加载器 使用的就是双亲委派模型; 主要有三种加载器: 1:Bootstrap clas...

  • 双亲委派模型

    类与类加载器 类加载器非常重要,因为每个类加载器都有一个独立的类名称空间。比如我们要加载两个类,如果要比较两个类是...

  • 双亲委派模型

    为何需要双亲委派模型: 如果你自己重写一个String类, 会发生什么?两个字:安全 JVM运行流程, JVM基本...

网友评论

      本文标题:08. 双亲委派模型

      本文链接:https://www.haomeiwen.com/subject/dcfafhtx.html