Tomcat 类加载需要完成的功能

多个 Web 应用程序的 Servlet 的相互隔离

假如在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。

多个 Web 应用程序 JAR 包的共享

假如两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring。Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。跟 JVM 一样,我们需要隔离 Tomcat 本身的类和 Web 应用的类。

如何隔离 Tomcat 本身的类和 Web 应用的类?


Tomcat 类加载器的层次结构

为了解决这些问题,Tomcat 设计了类加载器的层次结构,它们的关系如下图所示。
image.png

Web 应用的类如何互相隔离?

通过 Context 容器进行隔离。一个 Context 容器对应一个 Web 应用,
每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。
背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。

这就相当于在 Java 虚拟机内部###创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。

Web 应用如何共享 JAR 包(类库)?

本质需求是多个 Web 应用共享库类,避免重复加载相同的类。

在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么只需把共享的类放到父加载器的加载路径下就可以。应用程序通过这种方式共享 JRE 的核心类。

因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

Tomcat 和 Web 应用的类如何相互隔离?

共享可以通过父子的关系,隔离可以通过兄弟关系。兄弟关系是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。

基于此,Tomcat 又设计一个类加载器 CatalinaClassLoader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?

Tomcat 和 Web 应用如何共享类库?

共享可以通过父子的关系,再增加一个 CommonClassLoader,作为 CatalinaClassLoader 和 SharedClassLoader 的父加载器。

CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。
WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。

Spring 的类加载问题

JVM 的潜规则

默认情况下,如果一个类由类加载器 A 加载,那么它的依赖类也由类加载器 A 加载。

Spring 加载业务类

Spring 如何加载业务类?调用Class.forName,进而调用 Spring 的加载器。

存在的问题

Web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免重复加载。Spring 作为共享的第三方 JAR 包,本身是由 SharedClassLoader 来加载的,Spring 又要去加载业务类。

按照 JVM 潜规则,加载 Spring 的类加载器 SharedClassLoader 也会加载业务类。
但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,这该怎么办呢?

线程上下文加载器

线程上下文加载器其实是一种类加载器传递机制。
为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。Tomcat 不仅为每个 Web 应用创建一个 WebAppClassLoader 类加载器,还在在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。

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

ref

https://time.geekbang.org/column/article/105711