Tomcat源码解析--类加载

476 阅读7分钟

在理清楚Tomcat的启动与执行流程后,不妨思考这么一个问题:Tomcat会部署多个项目(将其读入内存),如果多个项目中存在同名类,项目会不会报错呢?肯定是不会的。那么它是怎么做到的呢?这就涉及到了Tomcat的类加载机制。

在了解Tomcat的类加载之前,首先需要看看JVM是怎么做的。

JVM类加载

主要是将.class文件以一个二进制字节流的方式读入到JVM中。

类加载过程:

1.通过类的全限定名获取该类的二进制字节流;

2.将字节流所代表的静态结构转化为方法区的运行时数据结构

3.在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类加载器

实现以上过程的代码模块就是“类加载器”。

类加载器分为以下几种:

启动类加载器(Bootstrap ClassLoader):该加载器使用C++语言实现,属于虚拟机自身的一部分。负责加载JAVA_HOME\lib目录中能被虚拟机识别的类库,如果名称不符合的类库即使在lib目录中也不会被加载。

扩展类加载器(Extension ClassLoader):该加载器主要负责加载JAVA_HOME\lib\ext目录中的类库,开发者可以使用扩展加载器。

应用程序类加载器(AppClassLoader):该加载器也称为系统加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

看一下三者的类图就会很清晰:

图中的ClassLoader就是启动类加载器,是由c++来实现类的加载,所以其中封装了native的load()。

找到URLClassLoader,查看它的实现类就能找到其他两种类加载器:

通过加载以上三个加载器对应的类库,即可看到调用:

双亲委派模型

我们都知道,类加载生成的Class对象是唯一的。那么这个对象是怎么保证唯一的呢?来源于同一个class文件,并且被同一个类加载器加载。

想象这样一个场景:HashMap本来是在ClassLoader中加载,如果将其放在classpath中,就又会被AppClassLoader加载一次。那么JVM中就会出现两个HashMap,场景十分混乱。

读取同一个class文件容易做到,但是JVM是怎么保证任何位置都是同一个类加载器的呢?

JVM是这么做的:如果一个类加载器收到了类请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成。层层递进,所有类加载的请求都会传到ClassLoader。只有当父加载器无法完成该请求时,子加载器才去自己加载。

将这个流程代入场景:HashMap本来由ClassLoader加载,将其放置在classpath后被AppClassLoader加载,它会一直向上直到ClassLoader。看Classloader能不能加载?能,ok。

以上就是双亲委派模型的工作方式,JVM通过其保证了类的唯一性。

双亲委派的实现

那么,在JVM中是怎么实现的呢?我们顺着上面的类图找一下就会发现,是直接通过super来调用。

自定义类加载器

只需要实现ClassLoader这个抽象类,然后重写loadClass()即可实现自定义类加载器。

值得注意的一点在于:在loadClass()中应该要遵循双亲委派,向上调用类加载器。如果不调用,就打破了双亲委派模型,Tomcat就是这么做的。

Tomcat类加载的前提

Tomcat下面有多个webapp,每个web应用的类库和Servlet应该互不影响。并且为了提高性能,如果一个Web应用重新部署时,该应用的类加载器重新加载,同时不会影响其他web应用。

实现这些要求只需要完成一个步骤,将类加载器机制抽离出来,不再沿用JVM的双亲委派,而保证每个web应用都有自己独立的一套类加载机制。

Tomcat的类加载器

Tomcat在AppClassLoader的基础上,自定义扩展了类加载器,打破了双亲委派,先看一眼它的实现结构:

那么,Tomcat是怎么部署并使用这些类加载器的呢?答案在BootStrap中。

//初始化三个类加载器以及确定父子关系
private void initClassLoaders() {
    try {
        // commonLoader的加载路径为common.loader(properties文件配置)
        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();
        }
        // 加载路径为server.loader,默认为空,父类加载器为commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // 加载路径为shared.loader,默认为空,父类加载器为commonLoader
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

commonLoader读取的是下面这个配置文件:

我们找到这个配置文件,根据上面debug出来的name找这个common.loader

其他两个配置均为空,结合createClassLoader也就印证了这三个类加载器,其实只是一个commonLoader。

这样设计其实是Tomcat良好扩展性的体现,从上面的结构图可以看出,Shared类加载器是被所有的webapp所共享的,我们可以自定义的抽离出来共性的模板;通过Catalina类加载器来完成一些服务器所需要而又不想对webapp可见的类库加载。

类加载工厂

Tomcat创建类加载器的过程是调用了一个工厂方法,将参数传给工厂由其来进行构建,此处不再跟进。

重点放在WebappLoader上,关注一下它的运作方式。这个加载器是从哪里产生的呢?

从server.xml得知,每个web应用会对对应一个Context节点,在JVM中就会对应一个StandardContext对象,所以我们来找一下StandardContext类。从启动流程分析,如果它有加载器,初始化的地方应该会放在initInternal()或者 startInternal()。参见Tomcat流程分析

我们在startInternal()中找到了其绑定的类加载器,并且将其封装成为该类的一个变量:

ok,找到WebappLoader类:

public class WebappLoader extends LifecycleMBeanBase
       implements Loader, PropertyChangeListener

可以看到最关键的一点,它被生命周期所管理,但是没有实现initInternal(),所以在 startInternal()中一定会有我们想要的答案。这个方法调用了createClassLoader()通过反射机制来实例化出了我们的WebappClassLoaderBase:

private WebappClassLoaderBase createClassLoader()
    throws Exception {

    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;
}
public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck

可以看到WebappClassLoaderBase实现了URLClassLoader,是真正的负责类加载的对象,它被

WebappLoader所持有。看一下它的loadClass(),发现其没有向上调用,所以Tomcat就在此打破了双亲委派机制。

热加载源码分析

通过上文,我们不难知道热加载其实就是一个类加载器重新读取的过程。热加载是针对于webapp的,那么哪个类是代表webapp呢?没错,StandardContext。在它的startInternal()中我们发现它启动了一个线程:

看一下这个线程执行了一些什么:run() --> processChildren()--> StandardContext。

最终又回到了StandardContext,我们看一眼这个方法:

这个loader我们是知道的,它是WebappClassLoader,直接debug进来:

@Override
public void backgroundProcess() {
    if (reloadable && modified()) {
        try {
            //设置线程类加载器的类加载器 为WebappClassloader
            Thread.currentThread().setContextClassLoader
                (WebappLoader.class.getClassLoader());
            if (context != null) {
                //终要执行的重新加载的方法:StandardContext类的reload():
                context.reload();
            }
        } finally {
            if (context != null && context.getLoader() != null) {
                Thread.currentThread().setContextClassLoader
                    (context.getLoader().getClassLoader());
            }
        }
    }
}

原来它是在此处实现了热加载,看if条件,如果你开启了roloadable选项,并且文件有被修改就会执行类加载器重新加载。