1.有哪些类加载器

首先要知道的是,把一个类加载进 JVM 指的是,通过 ClassLoader 把这个类的 class 文件读入后生成了相应 Class 对象。

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同路径加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的服务地址来加载。

JVM 的 ClassLoader 大致分为四类:

  • BootStrap ClassLoader:加载 $JAVA_HOME 中 jre/lib/rt.jar 中所有 class
  • ExtClassLoader:加载 $JAVA_HOME 中 jre/lib/ext 目录下的所有 jar 包
  • AppClassLoader:加载 classpath 下的所有 class(由 AppClassLoader 负责加载应用的 main 函数)
  • CustomClassLoader:需用户自行创建,可以是现有的 URLClassLoader 实例,也可以是用户自己继承 ClassLoader 后自定义类的实例

这里再说一下 URLClassLoader,用户只要传递规范的 URL,它就可以加载指定路径的类库或者 jar 包(包括本地磁盘及网络资源)。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类(ps:BootStrapClassLoader 是 JVM 的一部分,由 C++ 实现)。

java 自定义classloader热更新 自定义classloader加载jar包_开发语言

2.类加载机制

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?

JVM类加载的机制主要有如下三种:

1)全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其它 Class 也将由将加载器负责载入,除非显式指定另一个类加载器载入。

换句话说,虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。那何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。而每个 Class 对象里面都有一个 ClassLoader 属性记录了当前的类是由谁来加载的。

所以,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

2)双亲委派

一个类加载器查找 Class 和 resource 时,是通过“委托模式”进行的,它首先判断这个 Class 是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到 Bootstrap ClassLoader,如果 Bootstrap classloader 找到了,直接返回;如果没有找到,则一级一级返回,最后到达自身去查找这些对象。

java 自定义classloader热更新 自定义classloader加载jar包_jvm_02

注:所说的父 Classloader 并不是继承关系,而是每个 classloader 都有一个成员变量 parent 保存了它的父 classloader,一般通过构造器或者 setter 传入。或者说是组合关系。

双亲委派主要体现在 ClassLoader#loadClass() 方法中:

PS:每个 ClassLoader 都有如下三个基础方法:

  • loadClass()
  • 入口,定义了 加载/寻找 Class 的策略。比如双亲委派,或者其他形式
  • 调用 findClass() 读入 class 文件,并生成 Class 对象
  • findClass()
  • 根据 class 名,在当前 ClassLoader 能处理路径中查找,如果能找到就将 class 文件读入
  • 调用 defineClass 生成 Class 对象
  • defineClass()
  • native 方法
  • 将字节数组生成 Class 对象
// resolve = false 不进行解析
protected Class<?> loadClass(String name, boolean resolve) 
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查看该类是否被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 获取当前类父类的加载器
                    if (parent != null) {
                        // 使用父类的加载器加载!!!
                        // 递归
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果 parent 为 null,说明父类加载器为引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
				
                // 当前类的加载器父类加载器未加载此类 or 当前类的加载器未加载此类
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 调用当前 Classloader 的 findClass
                    // 注:可能当前 classloader 无法处理要加载的这个类的路径,这时返回 null
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 根据入参决定是否解析
            if (resolve) { 
                resolveClass(c);
            }
            return c;
        }
}

双亲委派机制的优势:

  • 采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。
  • 考虑到安全因素,java 核心api中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.String 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.String,而直接返回已加载过的 String.class,这样便可以防止核心API库被随意篡改。
3)缓存机制

缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓冲区中。这就是为什么修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因。

3.类加载时机

1)主动引用

Java类加载会初始化的情况有且仅有以下几种

  • 创建类的实例,也就是new一个对象
  • 访问某个类或者接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“com.lyj.load”))
  • 初始化一个类的子类(会首先初始化子类的父亲)
  • JVM启动时表明的启动类,即文件名和类名相同的那个类

除以上几种方法外,所有引用类的方法都不会触发初始化,称为被动引用。

2)被动引用

被动引用的例子:

  • 通过子类来引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化。
  • superclass () sc = new superclass[]; //不会触发superclass初始化,因为底层实现是直接生成object子类。
  • 引用一个类的静态常量也不会触发初始化,因为常量在编译阶段已经确认。

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

PS:接口也会有初始化的过程,但接口中不能有static块,但编译器也会为接口生成类构造器,用于初始化接口中成员变量,接口子类的不同仅是和『初始化一个类的子类(会首先初始化子类的父亲)』不同,因为接口不要求父接口全部实现,而是用到哪些实现哪些。

4.如何自定义 ClassLoader

在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。
  
下面是一个自定义 ClassLoader 示例,直接继承 ClassLoader 类,然后重写 findClass 方法就行了(采用双亲委派)

public class MyClassLoader extends ClassLoader{
	
    // 指定的要加载 class 文件的目录
    private File classPathFile;  
    
    public MyClassLoader(String absolutePath) {
        this.classPathFile = new File(absolutePath);
    }

    @Override
    // 根据类名将指定类加载进 JVM
    // 注:该 class 必修在指定的 absolutePath 下
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        // 拼接全类名
        String className = MyClassLoader.class.getPackage().getName() + "." + name;
        if(classPathFile  != null){
            // 根据绝对路径,以及 class 文件名,拿到 class 文件
            // 注意全类名的情况 
            File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class");
            // 如果 class 文件存在
            if(classFile.exists()){

                // 将 class 文件读入内存,暂存到一个字节数组中
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try{
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte [] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1){
                        out.write(buff,0,len);
                    }

                    // 构造类的 Class 对象!!!
                    // 注:defineClass() 是一个 native 方法
                    return defineClass(className, out.toByteArray(), 0, out.size());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}
PS:一定注意,对于用户自己创建的 ClassLoader 实例,会有一个默认的父 ClassLoader – SystemClassLoader(系统类加载器,一般是 AppClassLoader)

java 自定义classloader热更新 自定义classloader加载jar包_开发语言_03

5.小结:隔离和共享

这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

在双亲委派下,不同的 ClassLoader 之间也会有合作,而它们之间的合作是通过 parent 属性来完成的。parent 除了有更高的加载优先级之外,还表达了一种共享关系,即当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。

java 自定义classloader热更新 自定义classloader加载jar包_开发语言_04

这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。