美文网首页Java面试题
java几种类加载器及如何自定义类加载器

java几种类加载器及如何自定义类加载器

作者: zhengaoly | 来源:发表于2022-01-14 16:58 被阅读0次

    ClassLoader作用

    • 类加载流程的"加载"阶段是由类加载器完成的。

    类加载器结构

    结构:BootstrapClassLoader(祖父)-->ExtClassLoader(爷爷)-->AppClassLoader(也称为SystemClassLoader)(爸爸)-->自定义类加载器(儿子)

    关系:看括号中的排位;彼此相邻的两个为父子关系,前为父,后为子

    注意,这里的父子关系并不是通过继承构建的,而是在创建子加载器时,将父加载器通过setParent设置进去的,也就是组合模式,而非继承模式

    BootstrapClassLoader

    • 下边简称为boot
    • C++编写
    • 为ExtClassLoader的父类,但是通过ExtClassLoader的getParent()获取到的是null(在类加载器部分:null就是指boot)
    • 主要加载:\Java\jdk1.6\jre\lib*.jar(最重要的就是:rt.jar)

    ExtClassLoader:

    • 下边简称为ext
    • java编写,位于sun.misc包下,该包在你导入源代码的时候是没有的,需要重新去下
    • 主要加载:\Java\jdk1.6\jre\lib\ext*.jar(eg.dnsns.jar)

    AppClassLoader:

    • 下边简称为app
    • java编写,位于sun.misc包下
    • 主要加载:类路径下的jar,也就是classpath下的类,包括编译后的classes文件夹下的class文件和jar包中的class文件

    自定义类加载器:

    • 下边简称为custom,自定义加载器的parent为AppClassLoader
    • 自己编写的类加载器,需要继承ClassLoader类或URLClassLoader,并至少重写其中的findClass(String name)方法,若想打破双亲委托机制,需要重写loadClass方法
    • 主要加载:自己指定路径的class文件

    类加载器之间的关系见下图

    image-20220114154738554

    不同类加载器的命名空间关系

    咱们先回顾一下命名空间的概念:

    • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。(请结合下图一起看,想明白)
    • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
    • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

    得出命名空间的关系如下:(请结合下图一起看,想明白)

    • 同一个命名空间的类是相互可见的。
    • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
    • 由父类加载器加载的类不能看见子加载器加载的类。
    • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。
    image-20220114155612769

    这样,子加载器的命名空间,包含了父加载器的命名空间,就可以保证,子加载器加载的类,可以使用父加载器加载的类。也就是说,父加载器加载的类,对子加载器可见,同级别加载器加载的类,不可见

    类的唯一性

    在运行期,一个类的唯一性是由以下2点共同决定:

    1. 该类的完全限定名(binary name)。(包+类名)
    2. 用于加载该类的[定义类加载器],即defining class loader。
      上述2点都一样,才代表该类(可以理解为该类的Class对象)是一样的。
      如果同样的名字,不同的类加载器加载,那么这2个类是不一样的。即使.class文件完全一样,.class文件路径一样,这2个类也是不一样的。

    双亲委托机制

    这也是类加载器加载一个类的整个过程。

    过程:假设我现在从类路径下加载一个类A,

    1)那么app会先查找是否加载过A,若有,直接返回;

    2)若没有,去ext检查是否加载过A,若有,直接返回;

    3)若没有,去boot检查是否加载过A,若有,直接返回;

    4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib*.jar下找到了指定名称的类,则加载,结束;

    5)若没找到,boot加载失败;

    6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext*.jar下找到了指定名称的类,则加载,结束;

    7)若没找到,ext加载失败;

    8)app加载器加载,若在类路径classpath)下找到了指定名称的类,则加载,结束;

    9)若没有找到,抛出异常ClassNotFoundException

    注意:

    • 在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程 ("解析"见 第四章 类加载机制
    • 类的加载过程只有向上的双亲委托,没有向下的查询和加载,假设是ext在E:\Java\jdk1.6\jre\lib\ext*.jar下加载一个类,那么整个查询与加载的过程与app无关。
    • 假设A加载成功了,那么该类就会缓存在当前的类加载器实例对象C中,key是(A,C)(其中A是类的全类名,C是加载A的类加载器对象实例),value是对应的java.lang.Class对象
    • 上述的1)2)3)都是从相应的类加载器实例对象的缓存中进行查找
    • 进行缓存的目的是为了同一个类不被加载两次
    • 使用(A,C)做key是为了隔离类,假设现在有一个类加载器B也加载了A,key为(A,B),则这两个A是不同的A。这种情况怎么发生呢?
      • 假设有custom1、custom2两个自定义类加载器,他们是兄弟关系,同时加载A,这就是有可能的了

    总结:

    • 从底向上检查是否加载过指定名称的类;从顶向下加载该类。(在其中任何一个步骤成功之后,都会中止类加载过程)
    • 双亲委托的好处:假设自己编写了一个java.lang.Object类,编译后置于类路径下,此时在系统中就有两个Object类,一个是rt.jar的,一个是类路径下的,在类加载的过程中,当要按照全类名去加载Object类时,根据双亲委托,boot会加载rt.jar下的Object类,这是方法结束,即类路径下的Object类就没有加载了。这样保证了系统中类不混乱。

    ClassLoader.java类提供了loadClass方法,确保双亲委派机制,如下

    /**
         * 根据指定的binary name加载class。
         * 步驟:
         * 假设我现在从类路径下加载一个类A,
         * 1)那么app会先查找是否加载过A(findLoadedClass(name)),若有,直接返回;
         * 2)若没有,去ext检查是否加载过A(parent.loadClass(name, false)),若有,直接返回;
         * findBootstrapClassOrNull(name) 3)4)5)都是这个方法
         * 3)若没有,去boot检查是否加载过A,若有,直接返回;
         * 4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib\*.jar下找到了指定名称的类,则加载,结束;
         * 5)若没找到,boot加载失败;
         * findClass(name) 6)7)8)9)都是这个方法
         * 在findClass中调用了defineClass方法,该方法会生成当前类的java.lang.Class对象
         * 6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext\*.jar下找到了指定名称的类,则加载,结束;
         * 7)若没找到,ext加载失败;
         * 8)app加载,若在类路径下找到了指定名称的类,则加载,结束;
         * 9)若没有找到,抛出异常ClassNotFoundException
         * 注意:在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程
         */
        protected synchronized Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            Class c = findLoadedClass(name);//检查要加载的类是不是已经被加载了
            if (c == null) {//没有被加载过
                try {
                    if (parent != null) {
                        //如果父加载器不是boot,递归调用loadClass(name, false)
                        c = parent.loadClass(name, false);
                    } else {//父加载器是boot
                        /*
                         * 返回一个由boot加载过的类;3)
                         * 若没有,就去试着在E:\Java\jdk1.6\jre\lib\*.jar下查找 4)
                         * 若在bootstrap class loader的查找范围内没有查找到该类,则返回null 5)
                         */
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //父类加载器无法完成加载请求
                }
                if (c == null) {
                    //如果父类加载器未找到,再调用本身(这个本身包括ext和app)的findClass(name)来查找类
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    
    image-20220114161133272

    ClassLoader类提供了findClass方法的空实现,jdk1.2以后,用户如果需要自定义类加载器,只需要继承自ClassLoader,然后覆写findClass方法,这样可以确保双亲委派机制。jdk1.2以前,用户需要重写loadClass方法。

    AppClassLoader(系统加载器)和ExtClassLoader(扩展类加载器)都继承自URLClassLoader,URLClassLoader覆写了findClass方法
    

    说明:

    • 该段代码中引用的大部分方法实质上都是native方法

    • 其中findClass方法的类定义如下:

      /**
           * 查找指定binary name的类
           * 该类应该被ClassLoader的实现类重写
           */
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              throw new ClassNotFoundException(name);
          }
      

      关于findClass可以查看URLClassLoader.findClass(final String name),其中引用了defineClass方法,在该方法中将二进制字节流转换为了java.lang.Class对象。

    递归基于栈实现。

    上述的代码如果不清楚递归的意义是看不清的。

    解释:

    • app的loadClass()方法执行到ext的loadClass(),这时候对于app_loadClass()中剩余的findClass()会在栈中向下压;
    • 然后执行ext_loadClass(),当执行到findBootstrapClassOrNull(name),这时候ext_loadClass()中剩余的findClass()也会从栈顶向下压,此时ext_loadClass()_findClass()仅仅位于app_loadClass()_findClass()的上方;
    • 然后执行findBootstrapClassOrNull(name),当boot检测过后并且执行完加载后并且没成功,boot方法离开栈顶;
    • 然后执行此时栈顶的ext_loadClass()_findClass()
    • 然后执行此时栈顶的app_loadClass()_findClass()

    这样,就完成了双亲委托机制。

    实验

    1. 定义pojo类
    public class MyPerson {
     
        private MyPerson myPerson;
     
        public void  setMyPerson(Object obj){
            this.myPerson = (MyPerson)obj;
        }
    }
    
    1. 定义自定义类加载器
    package com.hisense.testbean.classloader;
    
    import java.io.*;
    
    public class CustomizedClassLoader extends ClassLoader {
    
        private String classLoaderName;
    
        private String path;
    
        private String fileExtension = ".class";
    
        public CustomizedClassLoader(String classLoaderName) {
            super();
            this.classLoaderName = classLoaderName;
        }
    
        public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
            super(parent);
            this.classLoaderName = classLoaderName;
        }
    
        @Override
        public Class<?> findClass(String className) throws ClassNotFoundException {
            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) {
            byte[] data = null;
            className = className.replace(".", "/");
            try(InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));
                ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                int ch;
                while(-1 != (ch = is.read())) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    }
    

    测试1

    public class TestClassLoaderNameSpace {
        public static void main(String[] args) throws Exception {
            CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
            CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
            String path = "";
            System.out.println(path);
            loader1.setPath(path);
            loader2.setPath(path);
            Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");
            Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");
    
            System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
            System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
            System.out.println( clazz1 == clazz2);
     
            Object object1 = clazz1.newInstance();
            Object object2 = clazz2.newInstance();
            Method method = clazz1.getMethod("setMyPerson", Object.class);
            method.invoke(object1, object2);
        }
    }
    

    设置path为“”,实际上时设置类的加载路径为classpath,此时根据类的加载机制

    第一次调用

    loader1.loadClass("com.hisense.testbean.classloader.MyPerson");
    

    CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),发现类属于classpath路径,加载类,通过defineclass完成加载类)

    第二次调用

    loader2.loadClass("com.hisense.testbean.classloader.MyPerson");
    

    CustomizedClassLoader(未找到)->app(已经加载过,缓存中找到)直接返回加载的类person

    因此,person的加载器实际上为AppClassLoader

    以上输出为:

    clazz1的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
    clazz2的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
    true
    

    测试2

    注意:

    需要删除target下的person.class文件,否则会优先通过app加载器,取classpath下加载,自定义的CustomizedClassLoader就不会加载了

    public class TestClassLoaderNameSpace {
        public static void main(String[] args) throws Exception {
            CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
            CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
            String path = "D:/test/";
            System.out.println(path);
            loader1.setPath(path);
            loader2.setPath(path);
            Class<?> clazz1 = loader1.loadClass("com.hisense.testbean.classloader.MyPerson");
            Class<?> clazz2 = loader2.loadClass("com.hisense.testbean.classloader.MyPerson");
    
            System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
            System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
            System.out.println( clazz1 == clazz2);
     
            Object object1 = clazz1.newInstance();
            Object object2 = clazz2.newInstance();
            Method method = clazz1.getMethod("setMyPerson", Object.class);
            method.invoke(object1, object2);
        }
    }
    

    手动把Person.java编译为Person.class,拷贝到"D:/test/"下,设置类的加载路径为"D:/test/",由于"D:/test/"不在系统classpath路径下,因此加载时,

    CustomizedClassLoader(未找到)->app(未找到)->ext(未找到,向bootstrap查找,调用findBootstrapClassOrNull(name))->ext(调用ext的findclass,不是/java/ext路径,递归栈返回)->app(调用app的findclass(),不是classpath下的路径,不加载,递归栈返回)->CustomizedClassLoader(调用自己的findClass方法,自己取D:/test/下加载类),加载完成,返回类

    此时,调用自定义加载器CustomizedClassLoader的findClass加载类,但是由于CustomizedClassLoader不是单例,此处我们定义了两个CustomizedClassLoader的实例loader1和loader2,loader1加载完以后,同级别的loader2并不知道person类的存在,因此loader2会再次加载一次。因此,二者不是同一个class类实例。彼此不可见

    输出

    D:/test/
    findClass invoked : com.hisense.testbean.classloader.MyPerson
    class loader name : loader1
    findClass invoked : com.hisense.testbean.classloader.MyPerson
    class loader name : loader2
    clazz1的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@340f438e
    clazz2的classLoader是com.hisense.testbean.classloader.CustomizedClassLoader@2d6e8792
    false
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:497)
        at com.hisense.testbean.classloader.TestClassLoaderNameSpace2.main(TestClassLoaderNameSpace2.java:23)
    Caused by: java.lang.ClassCastException: com.hisense.testbean.classloader.MyPerson cannot be cast to com.hisense.testbean.classloader.MyPerson
        at com.hisense.testbean.classloader.MyPerson.setMyPerson(MyPerson.java:8)
        ... 5 more
    

    报错,说明两个person类时不可见的,因为二者时不同加载器加载的

    相关文章

      网友评论

        本文标题:java几种类加载器及如何自定义类加载器

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