聊聊类加载器与双亲委派模型

1,998 阅读8分钟

前言

我们经常会在面试中遇到有关类加载器的问题,而作为一名Java开发人员应该了解类加载器如何工作?双亲委派模型是什么?如何打破双亲委派?为什么打破?等等。所以今天的主题就是聊一聊类加载器。

ClassLoader 介绍

《深入理解Java虚拟机》这本书大家都不陌生,想必我们大多数人了解JVM知识都是通过这本书,在该书中也详细介绍了Java类加载的全过程,包含加载、验证、准备、解析和初始化这5个阶段。

class loading

在加载阶段,通过一个类的全限定名来获取此类的二进制字节流,就是依靠类加载器来完成。

类加载器的一个作用就是将编译器编译生成的二进制 Class 文件加载到内存中,进而转换成虚拟机中的类。Java系统提供了三种内置的类加载器:

  • 启动类加载器 (Bootstrap Class Loader): 负责加载JDK核心类,通常是 rt.jar 和位于 $JAVA_HOME/jre/lib 下的核心库.
  • 扩展类加载器 (Extensions Class Loader): 负责加载\jre\lib\ext目录下 JAR 包
  • 系统类加载器 (System Class Loader):负责加载所有应用程序级别的类到JVM,它会加载classpath环境变量或 -classpath以及-cp命令行参数中指定的文件

当然,上面是 Java 默认的类加载器,我们还可以自定义类加载器,后文会分析如何自定义类加载器。

双亲委派模型是什么

网上有文章分析说,类加载器遵循三个原则:委托性可见性唯一性原则。这三点其实都和双亲委派模型有关,双亲委派的工作过程如下:

当类加载器收到类的加载请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求会传送到顶层的启动类加载器,只有父类加载器无法完成加载请求,才会交由子加载器去加载。

classLoader

三个原则的具体体现是:

  • 「委托性原则」 体现在当子类加载器收到类的加载请求时,会将加载请求向上委托给父类加载器。

  • 「可见性原则」 体现在允许子类加载器查看父类加载器加载的所有类,但是父类加载器不能查看子类加载器加载的类。

  • 「唯一性原则」 体现在双亲委派整个机制保证了Java类的唯一性,假如你写了一个和JRE核心类同名的类,比如Object类,双亲委派机制可以避免自定义类覆盖核心类的行为,因为它首先会将加载类的请求,委托给ExtClassLoader去加载,ExtClassLoader再委托给BootstrapClassLoader,启动类加载器如果发现已经加载了 Object类,那么就不会加载自定义的Object类。

ClassLoader 如何工作

聊完双亲委派模型,你肯定想知道它是如何实现,那么来看一下 ClassLoader 的核心方法,其中的 loadClass 方法就是实现双亲委派机制的关键,为了缩短代码篇幅和方便阅读,去掉了一些代码细节:

package java.lang;
public abstract class ClassLoader {

    protected Class defineClass(byte[] b); 
  
    protected Class<?> findClass(String name); 

    protected Class<?> loadClass(String name, boolean resolve) {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //2. 委托给父类加载
                        c = parent.loadClass(name, false);
                    } else {
                        //3. 父类不存在的,交给启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { }
                if (c == null) {
                    //4. 父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载
                    c = findClass(name);
                }
            }
            return c;
    }
}
  • defineClass 方法:调用 native 方法将 字节数组解析成一个 Class 对象。
  • findClass 方法:抽象类ClassLoader中默认抛出ClassNotFoundException,需要继承类自己去实现,目的是通过文件系统或者网络查找类
  • loadClass 方法: 首先根据类的全限定名检查该类是否已经被加载过,如果没有被加载,那么当子加载器持有父加载器的引用时,那么委托给父加载器去尝试加载,如果父类加载器无法完成加载,再交给子类加载器进行加载。loadClass方法 就是实现了双亲委派机制。

现在我们熟悉了 ClassLoader 的三个重要方法,那么如果需要自定义一个类加载器的话,直接继承 ClassLoader类,一般情况只需要重写 findClass 方法即可,自己定义加载类的路径,可以从文件系统或者网络环境。

但是,如果想打破双亲委派机制,那么还要重写 loadClass 方法,只不过,为什么我们要选择去打破它呢? 我们常使用的 Tomcat的类加载器就打破了双亲委派机制,当然还有一些其他场景也打破了,比如涉及 SPI 的加载动作、热部署等等。

接下来来看看 Tomcat 为什么打破双亲委派模型以及实现机制。

Tomcat如何打破双亲委派机制

为什么打破

现在都流行使用 springboot 开发 web 应用,Tomcat 内嵌在 springboot 中。而在此之前,我们会使用最原生的方式,servlet + Tomcat 的方式开发和部署 web 程序。web 应用的目录结构大致如下:


| -  MyWebApp
      | -  WEB-INF/web.xml        -- 配置文件,用来配置Servlet等
      | -  WEB-INF/lib/           -- 存放Web应用所需各种JAR包
      | -  WEB-INF/classes/       -- 存放你的应用类,比如Servlet类
      | -  META-INF/              -- 目录存放工程的一些信息

一个 Tomcat 可能会部署多个这样的 web 应用,不同的 web 应用可能会依赖同一个第三方库的不同版本,为了保证每个 web 应用的类库都是独立的,需要实现类隔离。而Tomcat 的自定义类加载器 WebAppClassLoader 解决了这个问题,每一个 web 应用都会对应一个 WebAppClassLoader 实例,不同的类加载器实例加载的类是不同的,Web应用之间通各自的类加载器相互隔离。

当然 Tomcat自定义类加载器不只解决上面的问题,WebAppClassLoader 打破了双亲委派机制,即它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用定义的类

如何打破

WebappClassLoader 具体实现机制是重写了 ClassLoader 的 findClass 和 loadClass 方法。

  • findClass 方法如下,省去部分细节:
public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    Class<?> clazz = null;
    try {
        //1. 先在 Web 应用目录下查找类 
        clazz = findClassInternal(name);
    } catch (RuntimeException e) {
        throw e;
    }
    if (clazz == null) {
        try {
            //2. 如果在本地目录没有找到,交给父加载器去查找
            clazz = super.findClass(name);
        }  catch (RuntimeException e) {
           throw e;
        }
    }
    //3. 如果父类也没找到,抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}
  • loadClass方法如下,省去部分细节:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;

        //1. 先在本地缓存查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            return clazz;
        }
        //2. 从系统类加载器的缓存中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }
        //3. 尝试用 ExtClassLoader 类加载器类加载
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 4. 尝试在本地目录搜索 class 并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // 省略
        }
    }
    //6. 上述过程都加载失败,抛出 ClassNotFoundException 异常
    throw new ClassNotFoundException(name);
}

从上面的代码中可以看到,Tomcat 自定义的类加载器确实打破了双亲委派机制,同时根据 loadClass 方法的核心逻辑,我也画了一张图,描述了默认情况下 Tomcat 的类加载机制。

Tomcat loadClass

一开始将类加载请求委托给 ExtClassLoader,而不是委托给 AppClassLoader,这样的原因是 防止 web 应用自己的类覆盖JRE的核心类,如果 JRE 核心类中没有该类,那么才交给自定义的类加载器 WebappClassLoader 去加载。

小结

这篇文章主要总结了类加载器的双亲委派模型、双亲委派的工作机制、以及Tomcat如何打破双亲委派,当然有一些东西分享的比较简单,比如 Tomcat 的类加载器这部分,没有提及整个 Tomcat的类加载器层次结构,没有提到 SharedClassLoader 和 CommonClassLoader 类加载器,这个等后续有时间再来分享。

同时,欢迎关注我新开的公众号,定期分享Java后端知识!

pjmike

参考资料 & 鸣谢