Java类加载器ClassLoader

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的双亲委派模型

Java类加载器ClassLoader_第1张图片
类加载器双亲委派模型

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

由此可见,所有的类最终都是由顶层的启动类加载器来加载完成。前面一节中描述了怎么判断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虚拟机类加载机制
  • Java Class文件结构解析
  • 类初始化造成的死锁
  • Java类加载器

你可能感兴趣的:(Java类加载器ClassLoader)