Tomcat源码分析 -- Tomcat类加载器

本章结构以下:html

  • 前言
  • Java类加载机制
  • tomcat类加载器
  • tomcat类加载器源码分析

1、前言

下载tomcat解压后,能够在webapps目录下看到几个文件夹(这些都是web应用),webapps对应到tomcat容器中的Host,里面的文件夹则对应到Context。tomcat启动后,webapps下的全部web应用均可以提供服务。java

那么就有一个问题,假如webapps下有两个应用app1和app2,它们有各自独立依赖的jar包,又有共同依赖的jar包,这些相同的jar包有些版本相同,有些又不相同,这种状况下,tomcat是如何加载这些jar包的呢?web

带着这个疑问,一步步来分析tomcat的类加载机制吧。spring

2、Java类加载机制

在这以前,固然要先了解一下java中类加载时怎样的,毕竟tomcat是用java写的,它的加载机制也是基于java的类加载机制。bootstrap

2.一、类加载器

1.什么是类加载器?缓存

虚拟机设计团队把类加载阶段中的“经过一个类的全限定名来获取描述此类的二进制字节流”这个动做放到Java虚拟机外部去实现,以便让应用程序本身决定如何去获取所须要的类。实现这个动做的代码模块称为“类加载器”。tomcat

2.如何判断两个类是否相等?服务器

类加载器用于实现类的加载动做。对于任意一个类,都须要由加载它的类加载器和这个类自己共同确立其在Java虚拟机中的惟一性,每个类,都拥有一个独立的类名称空间。也就是说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。不然,即便这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不一样,那这两个类就一定不相等。app

2.二、双亲委派模型

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

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

应用程序都是由这3种类加载器互相配合进行加载的,若是有必要,还能够加入本身定义的类加载器。加载流程以下图所示:

在这里插入图片描述

用一段代码测试一下:

public static void main(String[] args) {
        ClassLoader loader = Xxx.class.getClassLoader();
        while (loader!=null){
            System.out.println(loader);
            loader = loader.getParent();
        }
    }

结果:

在这里插入图片描述

从结果咱们能够看出,默认状况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader 的父加载器为 ExtClassLoader,可是 ExtClassLoader 的父加载器却显示为空,这是什么缘由呢?究其原因,启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中没法直接引用,因此才返回空。

java这种类加载层级称为双亲委派模型。它的工做过程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试本身去加载。

为何要这样呢?

都知道java.lang.Object是java中全部类的父类,它存放在rt.jar之中,按照双亲委派模型,不管哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,所以Object类在程序的各类类加载器环境中都是同一个类。试想,若是没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户彻底能够恶意编写一个java.lang.Object类,而后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就没法保证,应用程序也将会变得一片混乱。

2.三、简单看下源码

protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
    synchronized(this.getClassLoadingLock(var1)) {
        //首先, 检查请求的类是否已经被加载过了  
        Class var4 = this.findLoadedClass(var1);
        if(var4 == null) {
            long var5 = System.nanoTime();

            try {
                if(this.parent != null) {
                    var4 = this.parent.loadClass(var1, false);
                } else {
                    var4 = this.findBootstrapClassOrNull(var1);
                }
            } catch (ClassNotFoundException var10) {
                //若是父类加载器抛出ClassNotFoundException 
                //说明父类加载器没法完成加载请求  
                ;
            }

            if(var4 == null) {
                long var7 = System.nanoTime();
                //在父类加载器没法加载的时候  
                //再调用自己的findClass方法来进行类加载  
                var4 = this.findClass(var1);
                PerfCounter.getParentDelegationTime().addTime(var7 - var5);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
                PerfCounter.getFindClasses().increment();
            }
        }

        if(var2) {
            this.resolveClass(var4);
        }

        return var4;
    }
}

从源码能够看出,ExtClassLoader 和 AppClassLoader都继承自 ClassLoader 类,ClassLoader 类中经过 loadClass 方法来实现双亲委派机制。整个类的加载过程可分为以下三步:

  1. 查找对应的类是否已经加载。
  2. 若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,不然调用启动类加载器加载(findBootstrapClassOrNull 再往下会调用一个 native 方法)。
  3. 若第二步加载失败,说明父类加载器没法完成加载请求 ,则调用当前类加载器加载。

详细能够参考这篇博文:

Java 类加载机制详解

2.四、打破双亲委派模型(来自《深刻理解Java虚拟机:JVM高级特性与最佳实践》)

双亲委派模型并非一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。

它很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之因此称为“基础”,是由于它们老是做为被用户代码调用的API,但世事每每没有绝对的完美,若是基础类又要调用回用户的代码,那该怎么办?

这并不是是不可能的事情,一个典型的例子即是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它须要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoader()方法进行设置,若是建立线程时还未设置,它将会从父线程中继承一个,若是在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。

有了线程上下文类加载器,就能够作一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所须要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动做,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的通常性原则。

还有“被破坏”是因为用户对程序的动态性的追求致使的,例如OSGi的出现。在OSGi环境下,类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为网状结构。

当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将以java.*开头的类委派给父类加载器加载。
2)不然,将委派列表名单内的类委派给父类加载器加载。
3)不然,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)不然,查找当前Bundle的Class Path,使用本身的类加载器加载。
5)不然,查找类是否在本身的Fragment Bundle中,若是在,则委派给Fragment Bundle的类加载器加载。
6)不然,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)不然,类查找失败。

3、tomcat类加载器

了解了java的双亲委派模型,如今回到正题上,tomcat的类加载器是怎么样的?

3.一、Web容器应该具有的特性

不难想象,一个功能健全的Web容器,它的类加载器必然有多个,由于它应该具有以下特性:

  • 隔离性:部署在同一个Web容器上的两个Web应用程序所使用的Java类库能够实现相互隔离。设想一下,两个Web应用,一个使用了Spring2.5,另外一个使用了教新的4.0,应用服务器使用一个类加载器,Web应用将会由于jar包覆盖而没法启动。
  • 灵活性:Web应用之间的类加载器相互独立,那么就能针对一个Web应用进行从新部署,此时Web应用的类加载器会被重建,并且不会影响其余的Web应用。若是采用一个类加载器,类之间的依赖是杂乱复杂的,没法彻底移出某个应用的类。
  • 性能:部署在同一个Web容器上的两个Web应用程序所使用的Java类库能够互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,若是把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,若是类库不能共享,虚拟机的方法区就会很容易出现过分膨胀的风险。

3.二、tomcat类加载器结构

了解了一款Web容器应该具有的特性,明白了Web容器的类加载器有多个,再来看tomcat的类加载器结构。

首先上张图,总体看下tomcat的类加载器:

在这里插入图片描述

能够看到在原先的java类加载器基础上,tomcat新增了几个类加载器,包括3个基础类加载器和每一个Web应用的类加载器,其中3个基础类加载器可在conf/catalina.properties中配置,具体介绍下:

  • 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应用均不可见。

默认状况下,Common、Catalina、Shared类加载器是同一个,但能够配置3个不一样的类加载器,使他们各司其职。

首先,Common类加载器复杂加载Tomcat应用服务器内部和Web应用都可见的类,如Servlet规范相关包和一些通用工具包。

其次,Catalina类加载器负责只有Tomcat应用服务器内部可见的类,这些类对Web应用不可见。好比,想实现本身的会话存储方案,并且该方案依赖了一些第三方包,固然是不但愿这些包对Web应用可见,这时能够配置server.load,建立独立的Catalina类加载器。

再次,Shared类复杂加载Web应用共享类,这些类tomcat服务器不会依赖。

相信看到这,引言中的疑问已经解开了吧。

那还有一个问题,若是有10个Web应用程序都是用Spring来进行组织和管理的话,能够把Spring放到Common或Shared目录下让这些程序共享。Spring要对用户程序的类进行管理,天然要能访问到用户程序的类,而用户的程序显然是放在/WebApp/WEB-INF目录中的,那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?

若是按主流的双亲委派机制,显然没法作到让父类加载器加载的类去访问子类加载器加载的类,但使用线程上下文加载器,可让父类加载器请求子类加载器去完成类加载的动做。spring加载类所用的Classloader是经过Thread.currentThread().getContextClassLoader()来获取的,而当线程建立时会默认setContextClassLoader(AppClassLoader),即线程上下文类加载器被设置为AppClassLoader,spring中始终能够获取到这个AppClassLoader(在Tomcat里就是WebAppClassLoader)子类加载器来加载bean,之后任何一个线程均可以经过getContextClassLoader()获取到WebAppClassLoader来getbean了。

接下来,看一看源码。

4、tomcat类加载器源码分析

1.Common/Catalina/Shared ClassLoader的建立

首先先看下tomcat的类加载器继承结构:

在这里插入图片描述

相信看到这会很疑惑,这和上面介绍的tomcat类加载器结构不同啊。

是这样的,双亲委派模型本不是经过继承实现的,而是组合,因此AppClassLoader没有继承自ExtClassLoader,WebappClassLoader也没有继承自AppClassLoader。至于Common ClassLoader,Shared ClassLoader,Catalina ClassLoader则是在启动时初始化的三个不一样名字的URLClassLoader。

来到BootStrap中看一下:

public static void main(String args[]) {
    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则进入bootstrap的init方法。

public void init() throws Exception {

    initClassLoaders();

    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);
    
    ...
}

init方法会调用initClassLoaders,一样也会将Catalina ClassLoader设置到当前线程设置到当前线程,进入initClassLoaders来看看。

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能够看到这三个基础类加载器所加载的资源恰好对应conf/catalina.properties中的common.loader,server.loader,shared.loader:

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

2.Common/Catalina/Shared ClassLoader的层次构建

Common/Catalina/Shared ClassLoader的建立好了,确定是要被使用的,是在哪里使用的呢?它们之间同Webapp ClassLoader又是怎么联系起来的?

在这里插入图片描述

既然sharedClassLoader被传入到Catalina中,就来看它的getParentClassLoader调用栈。

通过层层调用,来带StandardContext的startInternal方法,这个方法很长很复杂,就不全贴出来,里面有这样一段:

if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}

它会建立WebappLoader对象,并经过setLoader(webappLoader)赋值到一个实例变量中,而后会调用WebappLoader的start方法:

...
if (ok) {
    // Start our subordinate components, if any
    Loader loader = getLoader();
    if (loader instanceof Lifecycle) {
        ((Lifecycle) loader).start();
    }
    ...
}
...

这里关系到tomcat的生命周期机制,先不纠结,直接找到start方法,start方法是在父类中,最后要调回到WebappLoader中的startInternal方法。

该方法中有这样一段:

...
classLoader = createClassLoader();
classLoader.setResources(context.getResources());
classLoader.setDelegate(this.delegate);
...

进入createClassLoader方法:

private WebappClassLoaderBase createClassLoader()
        throws Exception {

    // private String loaderClass = ParallelWebappClassLoader.class.getName();
    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

该方法会实例化一个ParallelWebappClassLoader实例,而且传递了sharedLoader做为其父亲加载器。

代码阅读到这里,已经基本清楚了Tomcat中ClassLoader的整体结构,总结以下: 在Tomcat存在common、cataina、shared三个公共的classloader,默认状况下,这三个classloader实际上是同一个,都是common classloader,而针对每一个webapp,也就是context(对应代码中的StandardContext类),都有本身的WebappClassLoader实例来加载每一个应用本身的类,该类加载实例的parent便是Shared ClassLoader。

这样前面关于tomcat的类加载层次应该就清楚起来了。

3.tomcat类加载器的加载过程

前面介绍了tomcat类加载器的建立及层次,下面进入本篇最后一点内容,这些类加载器是怎样加载类的呢?

因此重点看ParallelWebappClassLoader(看名字就是并行的WebappClassLoader,具体的差别就不作研究了)。

ParallelWebappClassLoader的loadClass是在其父类WebappClassLoaderBase中实现的:

第一步:

在这里插入图片描述

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

在这里插入图片描述

WebappClassLoader 加载过的类都存放在 resourceEntries 缓存中。protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();

第二步:

在这里插入图片描述

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

在这里插入图片描述

第三步:

在这里插入图片描述

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

protected ClassLoader getJavaseClassLoader() {
    return javaseClassLoader;
}

在这里插入图片描述

第四步:

在这里插入图片描述

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

如果默认的话,是先使用WebappClassLoader本身处理加载类的。固然,如果委托了,使用双亲委托亦没有加载到class实例,那仍是最后使用WebappClassLoader加载。

第五步:

在这里插入图片描述

如果没有委托,则默认会首次使用WebappClassLoader来加载类。经过自定义findClass定义处理类加载规则。

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

去阅读里面的代码,里面有这样一个方法:

@Override
public WebResource getClassLoaderResource(String path) {
    return getResource("/WEB-INF/classes" + path, true, true);
}

第六步:

在这里插入图片描述

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

最后借tomcat官网上的话总结一下:

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

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

tomcat提供了delegate属性用于控制是否启用java委派模式,默认false(不启用),当设置为true时,tomcat将使用java的默认委派模式,这时加载顺序以下:

(1).先从缓存中加载; (2).若是没有,则从JVM的Bootstrap类加载器加载; (3).若是没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。 (4).若是没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);