【tomcat调优系列4】Tomcat 类加载机制,和热部署

Tomcat类加载机制

JVM类加载器

Java中有3个类加载器,并且你可以自定义加载器。

  • BoostrapClassLoader 是启动类加载器,由C预演实现,用来加载JVM启动时所需的核心类,比如rt.jar。
  • ExtClassLoader是扩展类加载器,用来加载\jre\lib\ext 目录下Jar包。扩展加载器的 #getParent()返回null,实际上扩展类加载器的
    父类加载器就是启动类加载器。
  • AppClassLoader是系统类加载器,用来加载ClassPath下的类。应用程序默认用它来加载类。程序可以通过#getSystemClassLoader()来获取系统类加载器。
  • 自定义加载器,用来加载自定义路径下的类。

双亲委派机制

加载某个类时会先委派父类加载器寻找目标类,找不到再委托上层父加载器,如果所有父加载器都找不到目标类,则在自己的类加载器路径中查找,并载入目标类。

图片

图片

上图:ClassLoader#loadClass加载过程

问题: 为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被夹在,防止API库被篡改。
  • 避免类的重复加载:当父亲已经加载了该类,就没有必要子ClassLoader再加载一次,保证加载类的唯一性。

Tomcat类加载机制

Tomcat作为Servlet容器,它负责加载我们的Servlet类,此外还负责加载Servlet所依赖的Jar包。并且Tomcat本身也是一个Java程序。
因此它需要加载自己的类和依赖的Jar包。

问题:Tomcat如何隔离Web应用的?

Tomcat 自定义了一个类加载器WebAppClassLoader,并且给每个Web应用创建一个类加载器实例,每个Context容器负责创建和维护一个
WebAppClassLoader加载器实例。其实现的原理就是不同的类加载器实例加载的类被认为是不同的类,即使它们的类名相同(不同类加载器
实例加载的类是相互隔离的)

Tomcat的自定义类加载器WebAppClassLoader打破了双亲委派机制。它首先自己尝试加载某个类,如果找不到再代理给父类加载器,其目的是
优先加载Web应用自己定义的类。具体实现就是重写ClassLoader的2个方法:findClass和loadClass.

findClass步骤:

  1. 在Web应用本地目录下查找要加载的类。
  2. 如果没有找到,交给父加载器去查找,它的付加载器就是上面提到的系统类加载器。AppClassLoader.
  3. 如果父加载器也没找到这个类,抛出ClassNotFound异常。

loadClass有6个步骤:

  1. 现在本地Cache查找该类是否加载过,也就是说Tomcat的类加载器是否已经加载过这个类。
  2. 如果Tomcat加载器没加载过这个类,再看系统类加载器是否加载过。
  3. 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双
    亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就
    是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
  4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
  5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
  6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。

本地cache -> ExtendClassLoader -> WebAppClassLoader -> AppClassLoader

所以本地可以覆盖jar包里的实现。

Tomcat类加载器的层次结构

Tomcat 拥有不同的自定义类加载器,以实现对各种资源库的控制。 Tomcat 主要用类加载器解决以下 4 个问题:
同一个 Web 服务器里,各个 Web 项目之间各自使用的 Java 类库要互相隔离。

  • 同一个 Web 服务器里,各个 Web 项目之间各自使用的 Java 类库要互相隔离。
  • 同一个 Web 服务器里,各个 Web 项目之间可以提供共享的 Java 类库 。
  • 为了使服务器不受 Web 项目的影响,应该使服务器的类库与应用程序的类库互相独立。
  • 对于支持 JSP 的 Web 服务器,应该支持热插拔( HotSwap )功能 。

Tomcat提供了四组目录供用户存放第三方类库:

  • 放置在/common目录中:类库可被Tomcat和所有的 Web应用程序共同使用。
  • 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对 Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对 Tomcat和其他Web应用程序都不可见。

Tomcat自定义了多个类加载器

  1. CommonClassLoader 加载 /common
  2. CatalinaClassLoader 加载 /server/
  3. SharedClassLoader 加载 /shared/
  4. WebappClassLoader 加载 /WebApp/WEB-INF/

图片

线程上下文加载器

在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。
思考:如果spring作为共享第三方jar包,交给SharedClassLoader加载,但是业务类在web目录下,不在SharedClassLoader的加载路径下,那spring如何加载web应用目录下的业务bean呢?

问题: 如果spring作为共享第三方jar包,交给SharedClassLoader加载,但是业务类在web目录下,不在SharedClassLoader的加载路径下,那spring如何加载web应用目录下的业务bean呢?

Tomcat为每个Web应用创建一个WebAppClassLoader加载器,并在启动Web应用的线程设置上下文加载器。这样Spring在启动时就将现成上下文加载器取出来,用来加载Bean。

线程上下文加载器是一种类加载器传递机制,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。
Thread.currentThread().getContextClassLoader()

线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的

线程上下文加载器,在SPI实现上用的比较多。

Tomcat热加载和热部署

在项目开发过程中,经常要改动Java/JSP 文件,但是又不想重新启动Tomcat,有两种方式:热加载和热部署。热部署表示重新部署应⽤,它的执⾏主体是Host。 热加载表示重新加载class,它的执⾏主体是Context。
热加载:在server.xml -> context 标签中 设置 reloadable=”true”

  • 热加载:在server.xml -> context 标签中 设置 reloadable=”true”
1
2
<Context docBase="D:\mvc" path="/mvc"  reloadable="true" />
热部署:在server.xml -> Host标签中 设置  autoDeploy="true"
  • 热部署:在server.xml -> Host标签中 设置  autoDeploy=”true”
1
2
<Host name="localhost"  appBase="webapps"
unpackWARs="true" autoDeploy="true">

他们的区别

  • 热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。
  • 热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。

问题: Tomcat是如何用后台线程来实现热加载和热部署的

Tomcat开启后台线程执行周期性任务

Tomcat 通过开启后台线程ContainerBase.ContainerBackgroundProcessor,使得各个层次的容器组件都有机会完成一些周期性任务。我们在实际工作中,往往也需要执行一些周期性的任务,比如监控程序周期性拉取系统的健康状态,就可以借鉴这种设计。
Tomcat9 是通过ScheduledThreadPoolExecutor来开启后台线程的,它除了具有线程池的功能,还能够执行周期性的任务。

图片

此后台线程会调用当前容器的 backgroundProcess 方法,以及递归调用子孙的 backgroundProcess 方法,backgroundProcess 方法会触发容器的周期性任务。

图片

有了 ContainerBase 中的后台线程和 backgroundProcess 方法,各种子容器和通用 组件不需要各自弄一个后台线程来处理周期性任务,这样的设计显得优雅和整洁。

热加载实现原理

有了 ContainerBase 的周期性任务处理“框架”,作为具体容器子类,只需要实现自 己的周期性任务就行。而 Tomcat 的热加载,就是在 Context 容器中实现的。Context 容 器的 backgroundProcess 方法是这样实现的:

1
2
3
4
5
6
7
8
//  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 容器及其所有子容器,子容器其实就是 Wrapper,也就是说 Wrapper 里面 Servlet 实例也被销毁了。

  1. 停止和销毁 Context 容器及其所有子容器,子容器其实就是 Wrapper,也就是说 Wrapper 里面 Servlet 实例也被销毁了。
  2. 停止和销毁 Context 容器关联的 Listener 和 Filter。
  3. 停止和销毁 Context 下的 Pipeline 和各种 Valve。
  4. 停止和销毁 Context 的类加载器,以及类加载器加载的类文件资源。
  5. 启动 Context 容器,在这个过程中会重新创建前面四步被销毁的资源。

在这个过程中,类加载器发挥着关键作用。一个 Context 容器对应一个类加载器,类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context 容器在启动过程中,会创建一个新的类加载器来加载新的类文件。

Tomcat热部署原理

热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context 对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括 Session。
Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的

Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的

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
// 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 应用:

  • 如果原来 Web 应用目录被删掉了,就把相应 Context 容器整个销毁掉。
  • 是否有新的 Web 应用目录放进来了,或者有新的 WAR 包放进来了,就部署相应的 Web 应用。

因此 HostConfig 做的事情都是比较“宏观”的,它不会去检查具体类文件或者资源文件是否有变化,而是检查 Web 应用目录级别的变化。