Tomcat类加载机制与热部署原理详解
1.Tomcat类加载机制详解
1.1 JVM类加载器
Java中有3种类加载器,当然你也可以自定义类加载器
- 引导类加载器(启动类加载器):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar
- 扩展类加载器:负责加载支撑JVM运行的JRE的lib目录下ext扩展目录中的核心jar包
- 应用程序类加载器(系统类加载器):负责ClassPath路径下的类包,主要就是加载你自己写的类
- 自定义类加载器:自己实现,负责加载自定义路径下的类包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class ClassLoaderDemo{
public static void main(String[] args){
// BootStrapClassLoader c/c++实现,java层面是获取不到的,会输出null
System.out.println(ReentrantLock.class.getClassLoader());
// ExtClassLoader
System.out.println(ZipInfo.class.getClassLoader());
// AppClassLoader
System.out.println(ClassLoaderDemo.class.getClassLoader());
System.out.println("===========JVM类加载器父子关系==============");
// AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
// ExtClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
// BootStrapClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
1.2 双亲委派机制
Java中的类加载依赖双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载目录中查找并载入目标类,双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载
为什么设计双亲委派机制?
- 沙箱安全机制:防止核心类库API被随意篡改
- 避免类的重复加载:父加载器已经加载了该类时,就没必要子类加载器再加载一次,保证被加载类的唯一性
ClassLocader#loadClass源码分析
1 | public abstract class ClassLoader { |
1.3 Tomcat为什么(如何)打破双亲委派机制?
Tomcat的主要目的是充当应用服务器并处理用户的业务请求,使用场景可能会单机部署多应用。所以Tomcat打破双亲委派机制,核心是为了解决多Web应用独立运行的类隔离需求:双亲委派要求类加载器优先委托父加载器加载类,而Tomcat中多个Web应用可能依赖同一类的不同版本(如不同Spring版本),若按双亲委派,父加载器(如CommonClassLoader)加载一个版本后,所有应用都只能使用该版本,会引发类版本冲突;因此Tomcat自定义了WebAppClassLoader等加载器,让每个Web应用的类加载器优先加载自身WEB-INF/classes和WEB-INF/lib下的类,仅在自身未找到时才委托父加载器,从而实现不同应用类的独立隔离,保证多应用在同一容器中互不干扰地运行。
Tomcat打破双亲委派具体实现就是重写ClassLoader的两个方法:findClass和loadClass,来改变双亲委派的类加载顺序,但是直接重写这两个实现依然需要保证核心类库被篡改,所以为了保证核心类库的安全性,并没有一开始就使用系统类型加载器加载,而是先查询本地目录缓存,为了避免本地目录下的类覆盖JRE的核心类,会先尝试用JVM扩展类加载器ExtClassLoader去加载
findClass方法
1 |
|
loadClass方法
1 |
|
1.4 Tomcat如何隔离Web应用
Tomcat作为Servlet容器,它负责加载我们的Servlet类,此外它还负责加载Servlet所依赖的JAR包。并且Tomcat本身也是一个Java程序,因此它需要加载自己的类和依赖的JAR包。Tomcat此时需要解决下面这三个问题:
- Tomcat运行了两个Web应用程序,两个Web应用中有同名的Servlet,但是功能不同,Tomcat需要同时加载和管理这两个同名的Servlet类,保证它们不会冲突。Tomcat是如何实现Web应用之间的所有类都完成隔离的?
- 两个Web应用都依赖同一个第三方的JAR包,比如Spring,Spring的JAR包被加载到内存后,Tomcat要保证这两个Web应用能够共享,也就是说Spring的JAR包只被加载一次,否则随着依赖第三方JAR包的增多,JVM的内存会膨胀,Tomcat是如何解决的?
- 跟JVM一样,我们需要隔离Tomcat本身的类和Web应用的类。Tomcat是怎么实现的?
也就是如何实现内部类隔离,三方JAR包资源共享,以及自身的隔离性
Tomcat类加载器的层次结构
为了解决这些问题,Tomcat设计了类加载器的层次结构,它们的关系如下图所示:
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器以及本身各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有的Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class,只对当前webapp可见,加载war包里相关的类,每个war包应用都有自己的WebAppClassLoader,实现相互隔离,加载对应各自的依赖版本
全盘负责委托机制
当一个ClassLoader装载一个类的时候,除非显式的使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入。
比如Spring作为Bean工厂,它需要创建业务类的实例,并且创建业务类实例之前需要加载这些类。Spring是通过Class.forName来加载业务类的,下面是forName的源码:
1 | public static Class<?> forName(String className){ |
forName的函数中,会默认使用调用者的类加载器去加载业务类。
Web应用之间共享的JAR包可以交给SharedClassLoader来加载,从而避免重复加载。Spring作为共享的第三方JAR包,它本身是由SharedClassLoader来加载的,Spring又要去加载业务类,按照前面的那条规则,加载Spring的类加载器也会用来加载业务类,但是业务类再Web应用目录下,不在SharedClassLoader的加载路径下,Tomcat是如何解决这个问题的?
线程上下文加载器
于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫做“线程上下文加载器”?因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个线程类加载器取出来使用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文类加载器取出来,用来加载Bean。源代码如下:
1 | c1 = Thread.currentThread().getContextClassLoader(); |
线程上下文加载器不仅仅可以用在Tomcat和Spring类加载器的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据库驱动的
2. Tomcat热加载和热部署
项目开发过程中,经常要改动Java/JSP文件,但是又不想重新启动Tomcat,有两种方式:热加载和热部署。热部署表示重新部署应用,它执行的主体是HOST。热加载表示重新加载class,它的执行主体是Context。
- 热加载:在server.xml -> context标签中 设置 reloadable = “true”
1
<Context docBase="C:\project" path="/project" reloadable="true"/>
- 热部署:在server.xml -> Host标签中 设置 autoDeploy=”true”它们的区别是:
1
<Host name="localhost" appBase="webapps" unpackWARS="true" autoDeploy="true" />
- 热加载的实现方式是Web容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,这个过程中不会清空Session,一般用在开发环境
- 热部署原理类似,也是由后台线程定时检测Web应用的变化,但它会重新加载整个Web应用。这种方式会清空Session,比热加载更加彻底,一般用于生产环境
2.1 Tomcat开启后台线程执行周期性任务
Tomcat 通过ScheduledThreadPoolExecutor(定时线程池) 管理后台周期性任务,核心是在容器初始化时(如 Service 启动阶段)创建固定数量的后台线程,绑定到 Catalina 生命周期中统一管理。典型场景包括:会话过期清理(定期扫描 HttpSession,销毁超时会话)、日志滚动(按时间 / 大小切割访问日志)、连接池维护(检测并回收空闲数据库连接)、集群节点心跳检测等。任务调度支持 “固定延迟”(如每隔 30 秒执行)或 “固定速率”(如每分钟执行一次),且线程池会自动处理任务异常,避免单个任务失败导致整个线程池崩溃,确保周期性任务稳定执行且不阻塞主线程。
2.2 Tomcat热加载实现原理
Tomcat 热加载的核心是基于类加载器的 “重新加载” 机制,仅针对 Web 应用内部的类和资源(如 WEB-INF/classes、WEB-INF/lib),无需重启整个 Tomcat 容器。具体逻辑:
- 后台线程(如 WebAppLoader 的监控线程)定期扫描 WEB-INF/classes 和 lib 目录下文件的 “最后修改时间”,对比上次扫描记录;
- 若检测到文件变化(如.class 文件更新、JAR 包替换),Tomcat 会销毁当前 Web 应用的WebAppClassLoader(应用级类加载器)(同时销毁其加载的所有类、Servlet 实例、线程资源);
- 创建新的 WebAppClassLoader,重新加载更新后的类和资源,初始化 Servlet、Spring 容器等组件,完成 “热加载”。该机制仅适用于开发环境(如调试时修改代码),且无法处理服务器级配置(如 server.xml)的变化,因这类配置依赖 Tomcat 核心类加载器,无法单独重新加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 1. 启动资源监控线程(位于WebAppLoader中)
public void start() {
// 启动定时监控线程(每2秒扫描一次资源变化)
monitorThread = new Thread(new ResourceMonitor(), "WebappResourceMonitor");
monitorThread.setDaemon(true);
monitorThread.start();
}
// 2. 资源监控逻辑(检测类或JAR包变化)
class ResourceMonitor implements Runnable {
private long lastModified = 0; // 上次修改时间
public void run() {
while (running) {
// 扫描WEB-INF/classes和lib目录的最后修改时间
long currentModified = getResourcesLastModified();
if (currentModified > lastModified) {
// 检测到变化,触发热加载
context.reload(); // 调用Context的重新加载方法
lastModified = currentModified;
}
Thread.sleep(2000); // 间隔2秒扫描
}
}
}
// 3. Context的reload方法(销毁旧类加载器,创建新的)
public void reload() {
// 销毁当前Web应用的类加载器和资源
WebappClassLoader oldLoader = this.loader;
oldLoader.stop(); // 释放类、Servlet实例等资源
// 创建新的类加载器,重新加载资源
this.loader = new WebappClassLoader(oldLoader.getParent());
this.loader.start(); // 加载更新后的类和配置
}
2.3 Tomcat热部署实现原理
Tomcat 热部署是整个 Web 应用的 “卸载 - 重新部署” 过程,支持替换应用的完整资源(包括 WAR 包、配置文件、静态资源等),可用于生产环境的版本更新(通常需短暂停止应用)。具体逻辑:
- 触发方式(手动复制新 WAR 包、通过 Manager 应用 / API 发送部署指令);
- 卸载旧应用:停止应用所有 Servlet 和 Filter,关闭数据库连接池、线程池等资源,销毁对应的 WebAppClassLoader 和应用上下文(Context),删除旧应用的临时目录;
- 部署新应用:解压新 WAR 包(若为 WAR 格式),创建新的 Context 和 WebAppClassLoader,加载新的类、资源和配置文件,初始化 Servlet、Listener 等组件,绑定到 Connector(连接器)接收请求。
与热加载的核心区别是:热部署针对 “整个应用” 的替换,可处理配置文件(如 web.xml)的变化;热加载仅针对 “类和资源” 的重新加载,依赖应用级类加载器,不涉及 Context 销毁。
1 | // 1. 热部署触发(检测webapps目录下的WAR包变化) |