美文网首页
Java类加载器ClassLoader

Java类加载器ClassLoader

作者: 云飞扬1 | 来源:发表于2018-03-09 18:07 被阅读56次

    1. 什么是类加载器?

    类加载的实际过程为:通过一个类的全限定名来获取描述此类的二进制字节流。我们把实现这个动作的代码模块成为“类加载器”。

    2. 怎么比较两个类"相等"?

    我们知道使用关键字instanceof,可以判断某个对象是否是某个Class的实例对象,但是一旦涉及到类加载器ClassLoader之后,就会出现很多令人迷惑的现象。
    我们来先看个具体例子:

    public class Test { 
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
            Test test = new Test();
            System.out.println(test instanceof Test);
            
            ClassLoader classLoader = new ClassLoader() {
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    try {
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new ClassNotFoundException(name);
                    }
                }
            };
            
            Object obj = classLoader.loadClass("Test").newInstance();
            System.out.println(obj.getClass());
            System.out.println(obj.getClass() == Test.class);
            System.out.println(test.getClass() == Test.class);
            System.out.println(obj instanceof Test);
        }
    }
    

    这段代码的运行结果为:

    true
    class Test
    false
    true
    false
    

    从结果中可以看到,obj对象的class也为Test,但是与Test.class确是“不相等”的,而test对象的class与Test.class是“相等”的。它们两者之间的区别是,前者是由我们自定义的ClassLoader加载出来的,而后者是由虚拟机默认的ClassLoader加载出来的。虽然两者都是同一份class文件,但是加载的ClassLoader确不同,这说明要判断两个类是否“相等”,是由2个因素来决定的:

    1. 一是class信息是否“相等”,这里的“相等”指的是描述类的class信息是一致的,包括包名一致、类名一致、类里的信息一致等;
    2. 另一个就是加载该class的ClassLoader是否是同一个。

    3. ClassLoader的双亲委派模型

    类加载器双亲委派模型

    双亲委派模型要求所有的类加载器都有一个父加载器,除了最顶层的启动类加载器之外。它的执行逻辑是:当一个类加载器收到加载类的请求时,它不会自己去尝试加载类,而是委托给其父类来加载,每一个层级都是如此,直至到达启动类加载器为止,如果父类加载器反馈自己无法加载时,子类才会自己尝试去加载类。

    由此可见,所有的类最终都是由顶层的启动类加载器来加载完成。前面一节中描述了怎么判断class是否“相等”,而双亲委派模型保证了同一个类都是由同一个ClassLoader来加载的,避免了class类型的不一致。

    我们可以通过一个实例来看看不同的类加载时的ClassLoader有什么不同:

    public class Test { 
        public static void main(String[] args) {                
            System.out.println(Test.class.getClassLoader());
            Object obj = new Object();
            System.out.println(obj.getClass().getClassLoader());
            List list = new ArrayList();
            System.out.println(list.getClass().getClassLoader());   
        }
    }
    

    执行结果为:

    sun.misc.Launcher$AppClassLoader@338bd37a
    null
    null
    

    可以看到,我们自定义的类Test是通过AppClassLoader来加载的,而Object、List的ClassLoader确是null,这是因为这些都是由启动类加载器来加载的,启动类加载器是采用c++写的,在java环境里无法获取到该类的实例,因此为null。

    同样,我们依次打印下每个ClassLoader的父ClassLoader:

    public class Test { 
        public static void main(String[] args) {                        
            ClassLoader loader = Test.class.getClassLoader();
            while (loader != null) {
                System.out.println(loader);
                loader = loader.getParent();
            }
        }
    }
    

    结果为:

    sun.misc.Launcher$AppClassLoader@20e90906
    sun.misc.Launcher$ExtClassLoader@234f79cb
    

    这里也与双亲委派模型里ClassLoader层次结构是一致的,这里需要注意的是,AppClassLoader并不是直接继承自ExtClassLoader的,它们是通过组合的方式来实现父子关系的。

    4. Class.forName()加载类

    Class类有个静态方法名为forName,可以通过类的字符串名加载返回代表该类的Class对象,它有两个重载的方法,一个只有一个参数,一个有三个参数,我们来先看看有3个参数的方法定义:

    public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
    

    这三个参数的含义如下:
    name: 类或接口的全限定名
    initialize:前面介绍类加载机制时有讲过,共有加载、验证、准备、解析、初始化、使用、卸载等步骤,该参数为true表示加载该类时会进行类的初始化,false表示不会进行类的初始化。
    loader:表示采用哪个ClassLoader来加载该类

    我们通过一个例子来看看,类加载时不同的参数会有什么不同的结果。

    public class Test { 
    
        public static int COUNT = 0;
        
        static {
            System.out.println("Test init...");
        }
        
        public static void printCount() {
            System.out.println("COUNT: " + COUNT);
            COUNT++;
        }
        
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {       
            ClassLoader classLoader = new ClassLoader() {
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    try {
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new ClassNotFoundException(name);
                    }
                    
                }
            };
            Test.printCount();
            Test.printCount();      
            Test.printCount();
            
            Class clazz = Class.forName("Test", false, classLoader);
            System.out.println("==========");
            Method m = clazz.getMethod("printCount", null);
            m.invoke(null, null);
        }
        
    }
    

    执行结果如下:

    Test init...
    COUNT: 0
    COUNT: 1
    COUNT: 2
    ==========
    Test init...
    COUNT: 0
    

    在该例子中,执行Test.printCount()时,首先会触发Test类的初始化,然后连续共执行了3次,COUNT的值应该为3。接着我们使用自定义ClassLoader又加载了Test类,并且initialize参数设置为false,所以并没有触发类的初始化。然后我们通过反射调用了刚加载的Test类的printCount()方法,发现这个时候触发了类的初始化,并且打印出COUNT的值为0,这都说明采用自定义ClassLoader加载的Test类,与虚拟机默认加载的Test类压根是不同的对象。

    如果把加载类的代码改为Class clazz = Class.forName("Test", true, classLoader),结果会是什么样呢?

    Test init...
    COUNT: 0
    COUNT: 1
    COUNT: 2
    Test init...
    ==========
    COUNT: 0
    

    这就很明显的看出initialize为true或者false时,其加载过程的不同了。

    那么另外一个方法的执行逻辑是什么呢?

    public static Class<?> forName(String className)
    

    其实相当于Class.forName(className, true, appClassLoader),也即采用默认的ClassLoader来加载类,并且在加载时会进行类初始化。

    5. 为什么要自定义类加载器?

    大部分情况下,我们都不需要自定义类加载器。但是默认的类加载器有一个局限性,就是它只能加载特定目录下的class文件,但是如果我们想要加载远程服务器上的class文件,或者就是一个符合class规范的二进制字节流,那么就需要自定义类加载器来实现了。

    现在流行的热修复、热部署技术,其实都是利用了自定义类加载器来实现的。以Android应用中的热修复技术为例,一般情况下安卓应用发布到应用市场后,用户下载安装应用软件,如果应用软件出了比较致命的bug,通常必须由用户重新下载更新新的安装包才能解决问题。这些都要求用户升级软件,但是热修复技术可以不用升级软件就能动态解决原有软件的致命bug。其核心原理就是原本发布的软件里,通常是采用自定义ClassLoader来加载执行代码的,当某些代码出现问题后,发布修复问题的补丁包代码,客户端获取到补丁包代码后,采用自定义来加载器来加载补丁包里的类,而不是加载原来有问题的类,这样就达到了不升级软件就能解决问题的目标。

    java类加载机制系列文章:

    相关文章

      网友评论

          本文标题:Java类加载器ClassLoader

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