ClassLoader和类加载机制

深入学习Java基础知识
2021-05-17 14:29 · 阅读时长7分钟
小课

Java程序启动时并不是把所有的类一次性都加载到内存中,而是当需要的时候才加载,比如说调用到类的静态方法、变量,实例化类等等,ClassLoader即类加载器,就是负责在需要的时候将类加载到JVM中。

一、ClassLoader介绍

在Java 8及以前,Java中有三个内置的加载器,分别是BootstrapClassLoader、ExtensionClassLoader和AppClassLoader,它们负责加载不同的类。

  • BootstrapClassLoader负责加载$JAVA_HOME/jre/lib目录下的类,这里都是一些JDK内部比较核心的类,比如java.lang.* java.util.*等等,它和其它类加载器不同的是它是由native实现,特别的是另外两个类加载器也是由它加载的。
  • ExtensionClassLoader负责加载$JAVA_HOME/lib/ext 目录或者java.ext.dirs系统变量指定目录下的类,主要是一些扩展类。
  • AppClassLoader负责加载classpath环境变量里面指定目录、用户编写及引用的第三方库下的类。

除了BootstrapClassLoader以外,每个类加载器都有自己的父加载器,保存在parent成员变量中,比如AppClassLoader的父加载器是ExtensionClassLoader,而ExtensionClassLoader的父加载器是BootstrapClassLoader,不过这里比较特殊的是由于BootstrapClassLoader是native实现的,它并没有保存在BootstrapClassLoader的parent成员变量中,当需要通过parent查找class的时候是通过findBootstrapClass方法来实现的。

ClassLoader和类加载机制
二、类的加载机制

当调用类加载器的loadClass方法加载类时,首先检查该类是否已经加载过,加载过则直接返回加载好的Class对象,没有加载过则调用父加载器的loadClass方法来加载类,父加载器同样是先检查是否加载过该类,加载过则直接返回加载好的Class对象,没有则继续调用父加载器的父加载器的loadClass方法,就这样一层一层调用下去,直到调用到BootstrapClassLoader加载器也没有加载该类,则返回一层一层调用类加载器findClass来加载类,如果加载成功则返回Class对象,都没有加载成功则抛出ClassNotFoundException,这种加载机制就是常说的双亲委派机制

ClassLoader和类加载机制

遵循双亲委派机制可以避免不同的类加载器重复加载同一个类,同时也可以避免Java库中的类不被替换,比如说我们自定义一个java.lang.String类,在该类加载时会发现BootstrapClassLoader已经加载过了,所以不会再加载我们自己实现的java.lang.String类。

注:当父加载器是BootstrapClassLoader时,不是调用loadClass方法,而是通过findBootstrapClass来加载。

注释
三、Thread中的Context ClassLoader

虽然双亲委派机制有一定的好处,但是由于单向委派的原则,使得它失去一些灵活性,首先我们要明白的是当我们要加载一个类时,默认是使用调用者的类加载器去加载该类,比如下面这段代码

public class Course {
    String name;
}

public class Main {
    public static void main(String[] args) {
        Course course = new Course();
    }
}

当加载Course类时,会使用Main类的类加载器去加载Course类,对于大部分情况,这种逻辑都没有问题,但是对于Java核心包里面的类来说,它们的类加载器是BootstrapClassLoader,而BootstrapClassLoader只能加载$JAVA_HOME/jre/lib目录下的类,如果在Java核心包里面需要加载第三方库下的类,就只能用Thread的里面的Context ClassLoader了,它是Thread类里面的一个成员变量,可以根据需求设置,默认是父线程的Context ClassLoader,而main线程的Context ClassLoader是上面提到的AppClassLoader。

public class Thread implements Runnable {
    ...
    private ClassLoader contextClassLoader;
    ...
    public void setContextClassLoader(ClassLoader cl) {
        ...
    }
}

比如说Java的服务提供者接口(Service Provider Interface,SPI),它们都是定义在Java核心包下,而一般实现都是在第三方jar包中,为了统一管理,简化操作,加载这些第三方服务的实现类的逻辑是放在java.util.ServiceLoader中,而java.util.ServiceLoader本身的类加载器是BootstrapClassLoader,如果直接使用它的类加载器去加载第三方库是加载不了的,所以就需要用当前线程中的Context ClassLoader去加载,如果不显式设置的话也就是AppClassLoader。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
四、Java 9中类加载的变化

Java 9引入了模块化,JDK自带的一些核心类也不存放在$JAVA_HOME/jre/lib/rt.jar中,而是根据不同模块分别放在了$JAVA_HOME/jmods下面的jmod文件中,为了安全性和兼容之前的逻辑,Java 9仍然保留了之前这三个内置的类加载器,但是为了实现模块系统,还是有一些改动。

  • 首先,Java 9移除了扩展机制,不再支持通过$JAVA_HOME/lib/ext来进行扩展,ExtensionClassLoader改名为PlatformClassLoader。
  • PlatformClassLoader和AppClassLoader不再继承于URLClassLoader,而是继承于java.base模块的BuiltinClassLoader。
  • BootstrapClassLoader负责加载Java中的核心模块,比如java.basejava.rmi等等,这些模块都是拥有全部的权限,而PlatformClassLoader加载一些Java库中不需要全部权限的模块,比如java.sqljdk.charsets等等。
  • PlatformClassLoader和AppClassLoader的类加载流程有细微的改动,它重写了loadClass方法,当需要加载的类没有被加载过,它不会直接委托给父加载器加载,而是先判断该类所属的模块是否已经加载,如果模块加载过则直接使用模块的加载器来加载该类,如果模块没有加载过,则委托给父加载器,继续执行双亲委派机制,具体可参考下图。
ClassLoader和类加载机制
classloader类加载机制双亲委派