Tomcat 中有哪些类加载器?
2026年02月05日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常不只是想听你背出几个类加载器的名字。其深层次的考察点在于:
- 对 Java 类加载器体系结构的理解深度:你是否清楚标准的双亲委派模型,以及 Tomcat 作为 Servlet 容器,为何需要对其进行定制和扩展。
- 对 Web 容器隔离性设计的掌握:Tomcat 的核心设计目标之一是实现 Web 应用之间的隔离,以及 Web 应用与容器自身的隔离。类加载器是实现这一目标的关键技术。
- 对 “打破双亲委派” 模型的实际应用场景的认识:面试官想了解你是否能说出 Tomcat 在哪些场景下(如加载 Servlet 类)打破了双亲委派,以及为什么必须打破。
- 解决实际问题的能力:能否联系实际,解释这种设计如何避免了类库版本冲突(例如,不同 WebApp 使用不同版本的
commons-lang),以及开发、部署时的注意事项。
核心答案
Tomcat 在标准 JVM 的引导类加载器、扩展类加载器、应用类加载器之外,构建了一套自己的、具有层次结构的类加载器体系,以实现隔离与共享。其核心类加载器包括:
Common ClassLoader:作为 Tomcat 自身的类加载器,其父加载器是AppClassLoader。它负责加载/lib目录下,对所有 Web 应用和 Tomcat 自身都可见的、通用的类库(如tomcat-juli.jar)。在 Tomcat 8.5 及以后,它被更细粒度地拆分,但概念类似。- ️
Catalina ClassLoader(Server ClassLoader):用于加载 Tomcat 服务器运行时自身的、内部使用的类。它与 Web 应用加载器隔离,确保容器核心不被 Web 应用影响。其父加载器通常是Common ClassLoader。 Shared ClassLoader(Webapp ClassLoader):用于加载所有 Web 应用共享的类库。其父加载器是Common ClassLoader。但注意,在 Tomcat 的默认配置中,每个 Web 应用都有自己的WebappClassLoader,且其父加载器是Shared ClassLoader(而非标准的父委派)。这就是实现隔离的关键。WebappClassLoader:每个 Web 应用独享一个。它优先从本 Web 应用的/WEB-INF/classes和/WEB-INF/lib目录加载类。关键点在于:它在加载类时,会首先尝试自己加载,如果找不到,才会委托给父加载器(Shared),这与标准的双亲委派顺序相反,被称为 “反向双亲委派” 或 “优先本地加载”。
深度解析
原理/机制
- 隔离性:每个
WebappClassLoader实例加载的类,在其命名空间内是唯一的。WebApp A 的WebappClassLoader加载的com.example.MyClass与 WebApp B 的同名类是完全不同的,互不可见,从而实现了完美的应用隔离。 - 反向双亲委派:
WebappClassLoader的loadClass方法重写了标准的委派逻辑。其大致流程是:- 检查本地已加载类的缓存。
- 如果未加载,首先尝试使用本加载器(即从
WEB-INF下)加载。 - 如果本加载器找不到,且该类不属于 J2EE 规范中要求由容器父加载器加载的类(如
javax.servlet.*),则委托给父加载器(Shared->Common->App)。 - 对于
javax.servlet.*等 API 类,则直接委托给父加载器,确保所有 Web 应用使用的是容器提供的统一版本。
- 共享性:通过
Common和Shared加载器,Tomcat 自身和所有 Web 应用可以共享核心、稳定的库(如数据库驱动、日志框架实现),避免重复加载,节省内存。
代码示例 (Tomcat 类加载顺序伪代码逻辑)
// 简化版 WebappClassLoader.loadClass 逻辑
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查是否已由本加载器加载过
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 2. 判断是否属于“必须由父加载器加载”的包 (如 javax., java., org.apache.catalina, org.apache.jasper 等)
if (isExcludedPackage(name)) {
try {
// 强制委派给父加载器
clazz = parent.loadClass(name);
return clazz;
} catch (ClassNotFoundException e) {
// 父加载器也找不到,继续往下走
}
}
// 3. 【关键】优先尝试从本Web应用的WEB-INF下加载 (打破双亲委派)
try {
clazz = findClass(name); // 搜索 WEB-INF/classes 和 WEB-INF/lib
return clazz;
} catch (ClassNotFoundException e) {
// 本应用没有,忽略,继续委托
}
// 4. 最后,委托给父加载器链 (Standard ClassLoader -> Common -> App -> ...)
try {
clazz = parent.loadClass(name);
return clazz;
} catch (ClassNotFoundException e) {
// 父加载器链也找不到
}
throw new ClassNotFoundException(name);
}
}
最佳实践与注意事项
- 放置依赖:Web 应用独有的、特定版本的 jar 包,应放在
/WEB-INF/lib下。所有 Web 应用共享的、稳定的 jar 包(如mysql-connector-java),可考虑放在 Tomcat 的/lib目录(对应Common加载器)以减少内存占用,但要谨慎评估版本升级对所有应用的影响。 - 避免类泄露:Web 应用在停止或热部署时,其对应的
WebappClassLoader实例应该被丢弃并等待 GC。但如果应用中有线程(如通过ThreadLocal)或静态变量持有由该加载器加载的类的引用,会导致类加载器无法被回收,造成内存泄露。这是 Tomcat 热部署的常见问题。 - 理解
ParallelWebappClassLoader:在 Tomcat 8 及以后,默认使用ParallelWebappClassLoader,它支持并行加载类,提升了性能。
常见误区
- 误以为
WebappClassLoader完全打破了双亲委派:它只是在特定步骤(加载 Web 应用自有类时)优先处理,对于 Java 核心库和 Servlet API 等,它依然遵守委派。 - 混淆
reloadable与热部署:在context.xml中设置reloadable="true"后,Tomcat 会监听WEB-INF下的类文件变化并触发 Web 应用重启(即销毁原WebappClassLoader并创建新的),这会造成 Session 丢失等副作用,并非真正的 “热替换”。生产环境通常不开启此功能。
总结
Tomcat 通过一套精心设计的、层次化的类加载器体系(特别是为每个 Web 应用创建独立的 WebappClassLoader 并采用 “反向双亲委派” 机制),完美地实现了 Web 应用之间的类隔离、Web 应用与容器的隔离,以及基础类库的共享,这是其作为多应用托管容器的基石设计。