coding……
但行好事 莫问前程

Tomcat源码解读『Tomcat类加载机制』

本篇文章我们来探索一下Tomcat的类加载机制,如果我们搜Tomcat的类加载机制,绝大多数结果都会给出答案——打破双亲委托机制。但是感觉很多文章介绍的都不是很清楚,所以本篇文章就从我的理解上来分析一下Tomcat的类加载机制,希望能讲的明白。

关于JVM类加载机制,我们在之前的文章Java编程拾遗『Java ClassLoader工作机制』 已经介绍过,有兴趣的可以去了解一下。

1. Tomcat类加载机制要考虑的问题

Tomcat作为Servlet容器,它负责加载我们的Servlet类,此外它还负责加载Servlet所依赖的 JAR 包。并且Tomcat本身也是也是一个Java程序,因此它需要加载自己的类和依赖的JAR包,所以可能要考虑这几个问题:

  1. 假如在Tomcat中运行了两个Web应用程序,两个Web应用中有同名的 Servlet,但是功能不同,Tomcat需要同时加载和管理这两个同名的Servlet 类,保证它们不会冲突,也就是说,Web应用之间的类需要隔离
  2. 假如两个Web应用都依赖同一个第三方的JAR包,比如Spring,那Spring的JAR包被加载到内存后,Tomcat要保证这两个Web应用能够共享,也就是说Spring的JAR包只被加载一次,否则随着依赖的第三方JAR包增多,JVM 的内存会膨胀
  3. Tomcat自身也是一个Java程序,需要隔离Tomcat本身的类和Web应用的类,避免相互影响,比如Web应用中定义了一个同名类导致Tomcat本身的类无法加载

所以,Tomcat是如何来解决这些问题的?答案是通过设计多层次的类加载器。

1.1 WebAppClassLoader

首先来看第一个问题,假如我们使用 JVM 默认AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时, AppClassLoader 会返回第一个 Servlet 类的 Class 实例, 这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。

因此 Tomcat 的解决方案是自定义一个类加载器WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个WebAppClassLoader 加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间, Web 应用之间通过各自的类加载器互相隔离。

1.2 SharedClassLoader

再来看第 2 个问题,本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。因此 Tomcat 的设计者又加了一个类加载器SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类, SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。

1.3 CatalinaClassloader

第 3 个问题,如何隔离 Tomcat 本身的类和 Web 应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器CatalinaClassloader,专门来加载 Tomcat 自身的类。

如果Tomcat 和各 Web 应用之间需要共享一些类时该怎么办?

1.4 CommonClassLoader

老办法,还是再增加一个 CommonClassLoader,作为CatalinaClassloader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被CatalinaClassLoader 和 SharedClassLoader 使用,而CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离。

1.5 线程类加载器

在 JVM 的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载。比如 Spring 作为一个 Bean 工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring 是通过调用Class.forName来加载业务类的,我们来看一下 forName 的源码:

可以看到在 forName 的函数里,会用调用者也就是 Spring 的加载器去加载业务类。

前面提到,Web 应用之间共享的 JAR 包可以交给SharedClassLoader 来加载,从而避免重复加载。Spring 作为共享的第三方 JAR 包,它本身是由SharedClassLoader 来加载的,Spring 又要去加载业务类,按照前面那条规则,加载 Spring 的类加载器也会用来加载业务类,但是业务类在 Web 应用目录下,不在SharedClassLoader 的加载路径下,这该怎么办呢?

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

赞(0) 打赏
Zhuoli's Blog » Tomcat源码解读『Tomcat类加载机制』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

zhuoli's blog

联系我关于我

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏