在理清楚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选项,并且文件有被修改就会执行类加载器重新加载。