Tomcat 的类加载机制是怎样的?
面试考察点
-
JVM 类加载基础:面试官不仅仅是想知道 Tomcat 的类加载器长什么样,更是想确认你是否真正理解双亲委派模型——它的好处是什么、限制又是什么。基础不牢的话,这道题很难答到点子上。
-
Tomcat 特殊需求理解:Tomcat 作为一个 Web 容器,要同时跑多个 Web 应用,它面临的问题(应用隔离、类库共享、热部署)普通 JVM 根本不用操心。你能不能把这些需求讲清楚,是这道题的加分关键。
-
设计取舍的判断力:Tomcat "破坏" 双亲委派不是瞎搞,而是有意为之。面试官想看你是否理解 "什么时候该遵守规则、什么时候该打破规则" 这种架构决策能力。
核心答案
先说结论:Tomcat 没有完全遵守 JVM 的双亲委派模型,而是设计了一套自己的类加载器体系,核心目的是实现 Web 应用之间的类隔离和类库共享。
JVM 默认的双亲委派模型要求类加载请求一路向上委托给父加载器,父加载器找不到才由自己加载。但 Tomcat 有几个刚需双亲委派满足不了:
- 应用隔离:两个 Web 应用都依赖了
commons-lang3,但版本不同(一个是 3.12,一个是 3.14),必须互不干扰 - 类库共享:Spring、日志框架这些公共类库,所有 Web 应用共享一份就行,没必要每个应用都加载一份
- 热部署:重新部署一个 Web 应用时,需要把旧的类加载器扔掉,创建一个新的,而不影响其他应用
所以 Tomcat 的做法是:该共享的共享,该隔离的隔离,灵活运用但不乱来。
深度解析
一、Tomcat 类加载器体系全景图
上图展示了 Tomcat 完整的类加载器体系。从上到下可以分为三层:
-
JVM 标准层(Bootstrap → Extension → Application):这三层就是 JVM 默认的双亲委派,Tomcat 没动它。
Bootstrap加载 JDK 核心类库(rt.jar),Extension加载扩展类库,Application加载 Tomcat 自身的启动类(比如catalina.sh跑起来的入口类)。 -
Tomcat 公共层(Common ClassLoader):这是 Tomcat 自定义的第一个类加载器,负责加载
$CATALINA_HOME/lib目录下的所有 jar 包。像servlet-api.jar、el-api.jar以及你放到 lib 目录下的公共类库(比如 Spring、日志框架),都由它来加载。所有 Web 应用共享这一层。 -
Web 应用隔离层(WebApp ClassLoader):这是整个体系的核心。 每个 Web 应用都有一个独立的
WebAppClassLoader,负责加载WEB-INF/classes和WEB-INF/lib下的类。应用 A 和应用 B 各有自己的WebAppClassLoader,互相看不到对方的类,实现了隔离。
最下面还有一层 JspClassLoader,专门处理 JSP 文件的热编译。JSP 修改后不需要重启整个应用,只需要扔掉旧的 JspClassLoader,创建一个新的重新编译就行。
二、WebAppClassLoader 的加载顺序——打破双亲委派的关键
这一块是面试官最想听到的。JVM 的双亲委派是 "先问父亲,父亲找不到再自己来",但 WebAppClassLoader 的策略不一样:
上图展示了 WebAppClassLoader 的加载逻辑。核心区别就一句话:普通的双亲委派是 "先问爹",Tomcat 的 WebApp 是 "先自己找"。
为什么要反着来?举个例子你就明白了:
假设你的 Web 应用用了 Spring 5.3,Tomcat 的 lib 目录下有个旧版的 Spring 4.x。如果严格遵循双亲委派,Spring 4.x 会被 CommonClassLoader 先加载,你的应用就只能用旧版,运行直接报错。先自己找,就能保证应用用自己的 WEB-INF/lib 下的新版 Spring,互不干扰。
但是!Tomcat 并不是无脑 "先自己找",对 Java 核心类(java.*)和 JDK 标准扩展类(javax.*)依然严格遵守双亲委派,必须由父加载器加载。这是安全要求,不然你自己写个 java.lang.String 把 JDK 的给替换了,那还得了?
源码层面的实现,在 Tomcat 的 WebappClassLoaderBase 中,关键的 loadClass() 方法大致逻辑如下(简化版):
public Class<?> loadClass(String name, boolean resolve) {
// 1. 检查是否已加载
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
// 2. 核心类(java.* 等)必须委派给父加载器,不能自己搞
if (isJavaInternalClass(name)) {
clazz = parent.loadClass(name);
} else {
// 3. 先在自己地盘找(WEB-INF/classes、WEB-INF/lib)
try {
clazz = findClass(name);
} catch (ClassNotFoundException e) {
// 4. 自己找不到,再委托给父加载器
clazz = parent.loadClass(name);
}
}
}
return clazz;
}
当然实际源码比这复杂得多,还有各种安全校验、缓存机制,但核心逻辑就是这个 "先自己后父亲" 的策略。
三、为什么 Tomcat 要 "破坏" 双亲委派?
严格来说,Tomcat 并不是 "破坏" 双亲委派,而是 在保证安全的前提下做了合理的调整。这背后有三个硬需求:
需求一:Web 应用隔离
一台 Tomcat 部署了 3 个应用,A 用 Spring 5.3,B 用 Spring 4.3,C 用 Spring 6.0。如果共用一个类加载器,Spring 的 BeanFactory 类只能加载一个版本,其他两个应用就废了。每个应用一个独立的 WebAppClassLoader,各自加载各自的,互不干扰。
需求二:公共类库共享
反过来说,3 个应用都用 servlet-api.jar,这个 jar 又跟具体业务无关。如果每个应用都加载一份,纯粹浪费内存。所以这类公共类库放到 $CATALINA_HOME/lib 下,由 CommonClassLoader 统一加载,大家共享。
需求三:热部署/热替换
开发阶段改了个 Java 类,希望不用重启 Tomcat 就能生效。Tomcat 的做法是:停掉旧的 WebAppClassLoader(它加载的所有类都会被 GC),然后创建一个新的 WebAppClassLoader 重新加载应用。旧加载器加载的类在新加载器里完全不可见,实现了 "热替换"。双亲委派模型下类一旦加载就不会卸载,根本做不到这一点。
四、Common、Catalina、Shared 三个加载器的关系
这块容易搞混,简单理一下:
| 类加载器 | 加载路径 | 作用 | 可见性 |
|---|---|---|---|
CommonClassLoader | $CATALINA_HOME/lib | 公共类库,Tomcat 和所有 Web 应用共享 | Tomcat 内部 + 所有 WebApp |
CatalinaClassLoader | 通过 server.xml 配置 | Tomcat 自身内部使用的类,Web 应用看不到 | 仅 Tomcat 内部 |
SharedClassLoader | 通过 server.xml 配置 | 所有 Web 应用共享但 Tomcat 内部不用的类 | 所有 WebApp |
默认情况下,CatalinaClassLoader 和 SharedClassLoader 的加载路径跟 CommonClassLoader 是一样的(指向同一个目录),所以看起来好像只有 CommonClassLoader 在干活。你可以通过修改 catalina.properties 中的 server.loader 和 shared.loader 来把它们区分开。
实际生产中,大部分团队就用默认配置,很少去改这两个。知道有这么回事就行。
面试高频追问
-
追问一:Tomcat 是如何实现热部署的?
核心就是换类加载器。Tomcat 监控
WEB-INF/classes和WEB-INF/lib目录的文件变化(通过文件最后修改时间判断),发现变化后,先把旧的WebAppClassLoader停掉(它加载的所有类就不可达了,等待 GC 回收),然后创建一个新的WebAppClassLoader重新加载整个应用的类。对 JVM 来说,新旧两个同名类是由不同的类加载器加载的,属于不同的类型,互不影响。 -
追问二:如果两个 Web 应用都用了 Spring,Spring 的 Bean 会不会冲突?
不会。每个 Web 应用有独立的
WebAppClassLoader,各自加载各自的 Spring 框架类。应用 A 的ApplicationContext和应用 B 的ApplicationContext在 JVM 看来是两个完全不同的类实例,互不干扰。代价就是内存占用会大一些,Spring 的类被加载了多份。 -
追问三:可以把 Spring 的 jar 包放到 Tomcat 的 lib 目录下共享吗?
技术上可以,但不推荐。放到
lib下就由CommonClassLoader加载了,所有应用共享一份。问题在于:如果某个应用需要不同版本的 Spring,就搞不定了;而且所有应用共享同一个 Spring 上下文,容易产生冲突。除非你非常确定所有应用都用同一个版本,否则还是让每个应用自带比较好。
常见面试变体
- "Tomcat 为什么打破了双亲委派模型?"
- "Tomcat 是怎么实现 Web 应用之间的类隔离的?"
- "JVM 的双亲委派模型在 Tomcat 中适用吗?"
记忆口诀
Common 管公共,WebApp 管隔离,JSP 管热加载;核心类走爹,业务类自己来。
三个层次记住就行:JVM 标准层照旧 → Common 层共享 → WebApp 层隔离(优先自己加载)。
总结
Tomcat 设计了一套自己的类加载器体系:CommonClassLoader 负责加载公共类库实现共享,每个 Web 应用有独立的 WebAppClassLoader 实现隔离。WebAppClassLoader 的加载策略是 "先自己后父亲"(优先加载 WEB-INF 下的类),但对 Java 核心类依然严格遵守双亲委派。这不是 "破坏",而是 Tomcat 在保证安全的前提下,为了满足应用隔离、类库共享和热部署的需求做的合理设计。面试时把这个设计动机讲清楚,比单纯背类加载器的名字值钱得多。
