Java类加载器

概述

“虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制”

Java虚拟机对类的加载都是在程序运行期间进行的,增加了性能开销,但带来灵活度,可以在运行时从网络或者本地加载一个二进制流作为程序运行代码的一部分。

那虚拟机启动时具体初始化哪些类呢?有这样5中情况:
1. 遇到new、getstatic、putstatic、invokestatic时,如果类没有初始化,会对其执行初始化。
2. 使用反射调用时会初始化。
3. 遇到初始化一个类,如果有父类,先对其初始化
4. 虚拟机启动时,虚拟机会初始化用户指定的主类(main方法)
5. java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

本文的重点在于类和类加载器。

类的唯一性

在编写代码时,我们通过报名+类名定位一个类。但对虚拟机来说,还需要算上类加载器:

加载类的类加载器+类本身

也就是说,同一个类被不同的加载器加载,也认为不是一个类。

类加载器

广义来说,存在两种类加载器。虚拟机自带的Bootstrap ClassLoader和java实现的外部加载器。细致一点有以下分类:
* Bootstrap ClassLoader:加载核心类库,加载rt.jar,i18n.jar等;
* Extention ClassLoader:扩展的类加载器,机JRE_HOME/lib/ext
* AppLoader: 加载class path下的类,默认情况下是.,可以通过-classpath或者-cp指定,还可以设置环境变量–CLASSPATH(命令会覆盖环境变量)。

  • 虚拟机加载class,也是按照这个顺序依次寻找。如果不是自定义加载器,一般我们也无需关心加载顺序,只需要关心类冲突。
  • classpath 除了手动指定之外,如果在classpath下的jar的manifest有Class-Path 属性,也会去指定目录查找。

从上到下的加载顺序,如下图:

加载器有个基本思想:每个加载器都有一个父加载器(不是继承),当要加载一个类的时候,先委托他的父加载器去加载。

双亲委派模型

双亲委派模式是类加载的最重要的实现方式,先从两个示例开始:

public static void main(String[] args) throws Exception {
        // 自定义一个加载器,细节先不用管
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?>loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b,0,b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        // 加载并初始化
        Object obj = myLoader.loadClass("com.sample.ClassLoaderTest").newInstance();
        // 验证
        System.out.println(obj instanceof ClassLoaderTest);
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());
}

false
com.thoreauz.bootlearn.ClassLoaderTest$1@75bd9247
jdk.internal.loader.ClassLoaders$AppClassLoader@4f8e5cde

执行结果false,说明不是一个类,验证了之前对类唯一性的定位。

再看示例2:

Object newInstance = myLoader.loadClass("java.lang.String").newInstance();
System.out.println(newInstance instanceof String);
System.out.println(String.class.getClassLoader());

true
null

为什么String就是同一个类呢,且加载器是null,因为它的加载器是jvm的启动类加载器。它的实现逻辑是:
1. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
2. 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索路径中没有找到所需的类)时,子加载器才会尝试自己去加载。

这就是双亲委派模型

自定义加载器

分3步:
1. 继承ClassLoader,允许指定一个父加载器,不指定默认分配一个,
2. 覆写findClass()方法。

就这么简单,为什么没有写委托的逻辑呢,上面的示例触发的方法是loadClass(),父类ClassLoader已经把相关工作给做了,委托父加载器加载不到时,会调用我们覆写的findClass(String name)方法。代码如下:

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }
    private byte[] loadClassData(String className) {
        InputStream is = getClass().getClassLoader().getResourceAsStream(className.replace(".", "/")+".class");
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        int len =0;
        try {
            while((len=is.read())!=-1){
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return byteSt.toByteArray();
    }
}

不提倡覆盖loadClass(),而是把加载逻辑写在findClass(),这样就可以保证新写出来的类加载器是符合双亲委派规则的。

上例中loadClassData()方法,主要就是加载类的二进制流,我们可以通过本地或者网络获取,具体看需求实现。

使用场景

  1. 隔离:很多框架和容器都自定义了加载器,避免和业务代码使用的jar包冲突。比如tomcat。
  2. 热部署:虚拟机自然不会监测文件变化自动更新,如果要热部署,需要在自己的类加载器实现相应逻辑。
  3. 加密: 比如从网络加载class,就可以加密它,然后在自定义加载器的findClass解密。
CONTENTS