【JVM】类加载器与双亲委派

类加载器,顾名思义,即是实现类加载的功能模块,负责将Class的字节码形式加载成内存形式的Class对象。字节码文件可来源于磁盘或者jar包中的Class文件,也可以来自网络字节流。

类加载器

在JVM中,内置了三个重要的类加载器,Application classLoader,Extension classLoader和Bootstrap classLoader。应用类加载器,扩展类加载器和启动类加载器。

  • Bootstrap classLoader启动类加载器:加载JAVA_HOME/lib/rt.jar下的核心类,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。使用C代码实现,Java无法访问。
  • Extension classLoader扩展类加载器:加载JAVA_HOME/lib/ext/*.jar中的扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头。
  • Application classLoader应用类加载器:加载Classpath环境变量里定义的路径中的jar包和目录。自己编写的代码和第三方jar都由该类加载器加载。

三种类加载器存在传递性。Application classLoader 加载类时,会先问问Extension classLoader是否加载过,会在再问问Bootstrap classLoader是否加载过。

每个Class对象里面都有一个classLoader属性记录当前类由哪个类加载器加载

双亲委派模型

双亲委派机制也很好理解,AppClassLoader只负责加载ClassPath下的class文件,需要加载系统类库时,会委托上级类加载器,BootstrapClassLoader和ExtensionClassLoader,去加载对应的类库,这就是所谓的“双亲委派模型”

下面通过源码来分析双亲委派的流程。此处的loadClass方法来源于类加载器抽象类ClassLoader。该方法是加载类的入口。用户可以继承ClassLoader来自定义类加载器。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);//检查名称为name的类是否被本类加载器加载过
        if (c == null) {// 为null表示没有被加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {//上级不为null
                    c = parent.loadClass(name, false);//递归调用上级类加载器loadClass方法
                } else {//上级为null,即表示上级是启动类加载器
                    c = findBootstrapClassOrNull(name);//委托启动类加载器在javaHome/jre/lib下寻找名称为name的类
                }
            } catch (ClassNotFoundException e) {
                // 如果上级类加载器没有找到名称为name的类,则在此处捕获ClassNotFoundException异常
            }

            if (c == null) {//c为null表示上级类加载器没有找到name类
                long t1 = System.nanoTime();
                c = findClass(name);//到本加载器的路径下寻找名称为name的类(例如扩展类加载器则是lib/ext/下)

                // 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;
    }
}

综上,类加载器加载类的具体流程如下:

  1. 先检查本类加载器是否加载类,如果已加载,结束,没加载进入下一步;
  2. 递归委派上级类加载器;
  3. 如果没有上级了,执行启动类加载器,查找类是否存在对应的路径下;
  4. 上级都没有找到类时,跳出递归,在本类加载器对应路径下查找类。

双亲委派的好处:根据双亲委派模型的特点,可以知道,越是基础的类,由越上层的类加载器来加载,如此一来,Java类随着类加载具备了一个带有优先级地层次关系。这样可以保证,若用户编写了与Java类库中的类重名的类,此类不会被加载,因为同名的类往往是会被委派给启动类加载器或扩展类加载器来加载。因此双亲委派机制可以保证Java程序的稳定性

破坏双亲委派

在双亲委派机制中,我们知道,基础的类由上级类加载器加载。双亲委派可以保证“基础类作为API被用户代码调用”这个场景能够准确运行。但是,可能会存在一些基础类调用用户代码的情况。例如,Java提供了很多服务提供者接口(Service Provider Interface, SPI),允许第三方为这些接口提供实现,常见的SPI实现有JDBC、JCE、JNDI、JAXP等。SPI的接口由Java核心库提供,而其实现代码则是属于应用程序的jar包(放进CLASSPATH中)。那么问题来了,核心库中的SPI的接口由启动类加载来加载,CLASSPATH中的实现类由应用类加载器来加载,此种应用场景下,启动类加载器是无法找到SPI的实现类的。

因此,需要通过某种特殊手段,来打破双亲委派,让上级类加载器找不到类时,调用能获取到目标类的下级类加载器来进行加载。jdk引入了“线程上下文类加载器”来解决此问题。

线程上下文类加载器

在Thread类中有一个成员变量,contextClassLoader,如下所示

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

这个contextClassLoader就是线程上下文类加载器,用于引用一个类加载器。可以通过setContextClassLoader方法进行设置,若不设置,线程会从父线程中继承一个类加载器。main线程的contextClassLoader是应用类加载器,因此默认情况contextClassLoader都指向AppClassLoader

按照双亲委派的机制,上级的BootStrapClassLoader无法委派下级AppClassLoader来加载类,但是可以通过线程中的contextClassLoader来获取到AppClassLoader进行类加载。如此一来,便可以打破双亲委派的层次结构来逆向使用类加载器。

上一篇:spi


下一篇:Java基础(018):Class.forName和ClassLoader的区别