Tomcat 第六篇:类加载机制

1. 引言

Tomcat 在部署 Web 应用的时候,是将应用放在 webapps 文件夹目录下,而 webapps 对应到 Tomcat 中是容器 Host ,里面的文件夹则是对应到 Context ,在 Tomcat 启动以后, webapps 中的所有的 Web 应用都可以提供服务。

这里会涉及到一个问题, webapps 下面不止会有一个应用,比如有 APP1 和 APP2 两个应用,它们分别有自己独立的依赖 jar 包,这些 jar 包会位于 APP 的 WEB-INFO/lib 这个目录下,这些 jar 包大概率是会有重复的,比如常用的 Spring 全家桶,在这里面,版本肯定会有不同,那么 Tomcat 是如何处理的?

2. JVM 类加载机制

说到 Tomcat 的类加载机制,有一个绕不开的话题是 JVM 是如何进行类加载的,毕竟 Tomcat 也是运行在 JVM 上的。

以下内容参考自周志明老师的 「深入理解 Java 虚拟机」。

2.1 什么是类的加载

类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象, Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被 「首次主动使用」 时再加载它, JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误( LinkageError 错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件

2.2 类生命周期

接下来,我们看下一个类的生命周期:

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

2.3 双亲委派模型

Java 提供三种类型的系统类加载器:

  • 启动类加载器(Bootstrap ClassLoader):由 C++ 语言实现,属于 JVM 的一部分,其作用是加载 <JAVA_HOME>\lib 目录中的文件,或者被 -Xbootclasspath 参数所指定的路径中的文件,并且该类加载器只加载特定名称的文件(如 rt.jar ),而不是该目录下所有的文件。启动类加载器无法被 Java 程序直接引用。
  • 扩展类加载器( Extension ClassLoader ):由 sun.misc.Launcher.ExtClassLoader 实现,它负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器( Application ClassLoader ):也称系统类加载器,由 sun.misc.Launcher.AppClassLoader 实现。负责加载用户类路径( Class Path )上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型的工作机制:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

为什么?

例如类 java.lang.Object ,它存放在 rt.jar 之中。无论哪一个类加载器都要加载这个类。最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载。因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户编写了一个称为 「java.lang.Object」 的类,并存放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类, java 类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。

3. Tomcat 类加载机制

先整体看下 Tomcat 类加载器:

可以看到,在原来的 JVM 的类加载机制上面, Tomcat 新增了几个类加载器,包括 3 个基础类加载器和每个 Web 应用的类加载器。

3 个基础类加载器在 conf/catalina.properties 中进行配置:

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

server.loader=

shared.loader=
  • Common: 以应用类加载器为父类,是 Tomcat 顶层的公用类加载器,其路径由 conf/catalina.properties 中的 common.loader 指定,默认指向 ${catalina.home}/lib 下的包。
  • Catalina: 以 Common 类加载器为父类,是用于加载 Tomcat 应用服务器的类加载器,其路径由 server.loader 指定,默认为空,此时 Tomcat 使用 Common 类加载器加载应用服务器。
  • Shared: 以 Common 类加载器为父类,是所有 Web 应用的父类加载器,其路径由 shared.loader 指定,默认为空,此时 Tomcat 使用 Common 类加载器作为 Web 应用的父加载器。
  • Web 应用: 以 Shared 类加载器为父类,加载 /WEB-INF/classes 目录下的未压缩的 Class 和资源文件以及 /WEB-INF/lib 目录下的 jar 包,该类加载器只对当前 Web 应用可见,对其他 Web 应用均不可见。

4. Tomcat 类加载机制源码

4.1 ClassLoader 的创建

先看下加载器类图:

先从 BootStrap 的 main 方法看起:

public static void main(String args[]) {
    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
        // 省略其余代码...
    }
}

可以看到这里先判断了 bootstrap 是否为 null ,如果不为 null 直接把 Catalina ClassLoader 设置到了当前线程,如果为 null 下面是走到了 init() 方法。

public void init() throws Exception {
    // 初始化类加载器
    initClassLoaders();
    // 设置线程类加载器,将容器的加载器传入
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    // 设置区安全类加载器
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // 省略其余代码...
}

接着这里看到了会调用 initClassLoaders() 方法进行类加载器的初始化,初始化完成后,同样会设置 Catalina ClassLoader 到当前线程。

private void initClassLoaders() {
    try {
        commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader = this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

看到这里应该就清楚了,会创建三个 ClassLoader : CommClassLoader , Catalina ClassLoader , SharedClassLoader ,正好对应前面介绍的三个基础类加载器。

接着进入 createClassLoader() 查看代码:

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
        }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

可以看到,这里加载的资源正好是我们刚才看到的配置文件 conf/catalina.properties 中的 common.loaderserver.loadershared.loader

4.2 ClassLoader 加载过程

直接打开 ParallelWebappClassLoader ,至于为啥不是看 WebappClassLoader ,从名字上就知道 ParallelWebappClassLoader 是一个并行的 WebappClassLoader 。

然后看下 ParallelWebappClassLoader 的 loadclass 方法是在它的父类 WebappClassLoaderBase 中实现的。

4.2.1 第一步:

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {
        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class<?> clazz = null;

        // Log access to stopped class loader
        checkStateForClassLoading(name);

        // (0) Check our previously loaded local class cache
        clazz = findLoadedClass0(name); 
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
        // 省略其余...

首先调用 findLoaderClass0() 方法检查 WebappClassLoader 中是否加载过此类。

protected Class<?> findLoadedClass0(String name) {

    String path = binaryNameToPath(name, true);

    ResourceEntry entry = resourceEntries.get(path);
    if (entry != null) {
        return entry.loadedClass;
    }
    return null;
}

WebappClassLoader 加载过的类都存放在 resourceEntries 缓存中。

protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

4.2.2 第二步:

    // 省略其余...
    clazz = findLoadedClass(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return clazz;
    }
    // 省略其余...

如果第一步没有找到,则继续检查 JVM 虚拟机中是否加载过该类。调用 ClassLoader 的 findLoadedClass() 方法检查。

4.2.3 第三步:

    ClassLoader javaseLoader = getJavaseClassLoader();
    boolean tryLoadingFromJavaseLoader;
    try {

        URL url;
        if (securityManager != null) {
            PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
            url = AccessController.doPrivileged(dp);
        } else {
            url = javaseLoader.getResource(resourceName);
        }
        tryLoadingFromJavaseLoader = (url != null);
    } catch (Throwable t) {

        ExceptionUtils.handleThrowable(t);

        tryLoadingFromJavaseLoader = true;
    }

    if (tryLoadingFromJavaseLoader) {
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

如果前两步都没有找到,则使用系统类加载该类(也就是当前 JVM 的 ClassPath )。为了防止覆盖基础类实现,这里会判断 class 是不是 JVMSE 中的基础类库中类。

4.2.4 第四步:

    boolean delegateLoad = delegate || filter(name, true);

    // (1) Delegate to our parent if requested
    if (delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

先判断是否设置了 delegate 属性,设置为 true ,那么就会完全按照 JVM 的”双亲委托”机制流程加载类。

若是默认的话,是先使用 WebappClassLoader 自己处理加载类的。当然,若是委托了,使用双亲委托亦没有加载到 class 实例,那还是最后使用 WebappClassLoader 加载。

4.2.5 第五步:

    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    try {
        clazz = findClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Loading class from local repository");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }
    } catch (ClassNotFoundException e) {
        // Ignore
    }

若是没有委托,则默认会首次使用 WebappClassLoader 来加载类。通过自定义 findClass() 定义处理类加载规则。

findClass() 会去 Web-INF/classes 目录下查找类。

4.2.6 第六步:

    if (!delegateLoad) {
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        try {
            clazz = Class.forName(name, false, parent);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }
    }

若是 WebappClassLoader 在 /WEB-INF/classes/WEB-INF/lib 下还是查找不到 class ,那么无条件强制委托给 System 、 Common 类加载器去查找该类。

4.2.7 小结

Web 应用类加载器默认的加载顺序是:

  1. 先从缓存中加载;
  2. 如果没有,则从 JVM 的 Bootstrap 类加载器加载;
  3. 如果没有,则从当前类加载器加载(按照 WEB-INF/classes 、 WEB-INF/lib 的顺序);
  4. 如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序是 AppClassLoader 、 Common 、 Shared 。

参考

//www.jianshu.com/p/69c4526b843d

Tags: