Tomcat类加载机制
Java中有 3 个类加载器,另外你也可以自定义类加载器
public class ClassLoaderDemo {
public static void main(String[] args) {
// BootstrapClassLoader
System.out.println(ReentrantLock.class.getClassLoader());
// ExtClassLoader
System.out.println(ZipInfo.class.getClassLoader());
// AppClassLoader
System.out.println(ClassLoaderDemo.class.getClassLoader());
// AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader());
// ExtClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent());
// BootstrapClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。这就是双亲委派机制。
思考:为什么要设计双亲委派机制?
Tomcat 作为 Servlet 容器,它负责加载我们的 Servlet 类,此外它还负责加载 Servlet 所依赖的 JAR 包。并且 Tomcat 本身也是也是一个 Java 程序,因此它需要加载自己的类和依赖的 JAR 包。
思考: Tomcat是如何隔离Web应用的?
Tomcat 自定义了一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。其实现的原理就是不同的类加载器实例加载的类被认为是不同的类,即使它们的类名相同(不同类加载器实例加载的类是互相隔离的)。
Tomcat 的自定义类加载器 WebAppClassLoader 打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写
ClassLoader 的两个方法:findClass 和 loadClass。
在 findClass 方法里,主要有三个步骤:
loadClass 方法稍微复杂一点,主要有六个步骤:
也就是说会先走ext扩展类加载器然后木有加载到 (Delegate这个标志一般都是false ,为true的话就优先走系统类加载器了)就走自己的加载器加载,自己的加载器没有就使用系统类加载器
Tomcat 拥有不同的自定义类加载器,以实现对各种资源库的控制。 Tomcat 主要用类加载器解决以下 4 个问题:
Tomcat自定义了多个类加载器,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。
思考:如果spring作为共享第三方jar包,交给SharedClassLoader加载,但是业务类在web目录下,不在SharedClassLoader的加载路径下,那spring如何加载web应用目录下的业务bean呢?
Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。
线程上下文加载器是一种类加载器传递机制,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。
Thread.currentThread().getContextClassLoader()
线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的
又比如
线程a:A类的加载器加载
b线程 :B类的加载器加载
b加载器里面有的类需要a的加载器 那就可以直接从a线程取出来加载
在项目开发过程中,经常要改动Java/JSP 文件,但是又不想重新启动Tomcat,有两种方式:热加载和热部署。热部署表示重新部署应⽤,它的执⾏主体是Host。 热加载表示重新加载class,它的执⾏主体是Context。
它们的区别是:
思考:Tomcat 是如何用后台线程来实现热加载和热部署的?
Tomcat 通过开启后台线程ContainerBase.ContainerBackgroundProcessor,使得各个层次的容器组件都有机会完成一些周期性任务。我们在实际工作中,往往也需要执行一些周期性的任务,比如监控程序周期性拉取系统的健康状态,就可以借鉴这种设计。
Tomcat9 是通过ScheduledThreadPoolExecutor来开启后台线程的,它除了具有线程池的功能,还能够执行周期性的任务。
此后台线程会调用当前容器的 backgroundProcess 方法,以及递归调用子孙的 backgroundProcess 方法,backgroundProcess 方法会触发容器的周期性任务。
有了 ContainerBase 中的后台线程和 backgroundProcess 方法,各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务,这样的设计显得优雅和整洁。
这里精髓的地方是使用定时任务去调度,而且调度这个
ContainerBackgroundProcessor还嵌套了一个监视器的定时任务ContainerBackgroundProcessorMonitor去调度ContainerBackgroundProcessor这个后台任务,
这个监视器任务也是周期性定时的,那么监视器任务挂了也可以定时周期启动
有了 ContainerBase 的周期性任务处理“框架”,作为具体容器子类,只需要实现自己的周期性任务就行。而 Tomcat 的热加载,就是在 Context 容器中实现的。Context 容器的 backgroundProcess 方法是这样实现的:
// StandardContext#backgroundProcess
//WebappLoader 周期性的检查 WEB-INF/classes 和 WEB-INF/lib 目录下的类文件
// 热加载
Loader loader = getLoader();
if (loader != null) {
loader.backgroundProcess();
}
WebappLoader 实现热加载的逻辑:它主要是调用了 Context 容器的 reload 方法,先stop Context容器,再start Context容器。具体的实现:
在这个过程中,类加载器发挥着关键作用。一个 Context 容器对应一个类加载器,类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context 容器在启动过程中,会创建一个新的类加载器来加载新的类文件。
热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context(StandardContext) 对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括 Session。
Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的
// HostConfig#lifecycleEvent
// 周期性任务
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
}
protected void check() {
if (host.getAutoDeploy()) {
// Check for resources modification to trigger redeployment
DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]);
for (DeployedApplication app : apps) {
if (tryAddServiced(app.name)) {
try {
// 检查 Web 应用目录是否有变化
checkResources(app, false);
} finally {
removeServiced(app.name);
}
}
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
//热部署
deployApps();
}
HostConfig 会检查 webapps 目录下的所有 Web 应用:
因此 HostConfig 做的事情都是比较“宏观”的,它不会去检查具体类文件或者资源文件是否有变化,而是检查 Web 应用目录级别的变化。