Tomcat 中有哪些类加载器?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官提出这个问题,通常不只是想听你背出几个类加载器的名字。其深层次的考察点在于:

  1. 对 Java 类加载器体系结构的理解深度:你是否清楚标准的双亲委派模型,以及 Tomcat 作为 Servlet 容器,为何需要对其进行定制和扩展。
  2. 对 Web 容器隔离性设计的掌握:Tomcat 的核心设计目标之一是实现 Web 应用之间的隔离,以及 Web 应用与容器自身的隔离。类加载器是实现这一目标的关键技术。
  3. 对 “打破双亲委派” 模型的实际应用场景的认识:面试官想了解你是否能说出 Tomcat 在哪些场景下(如加载 Servlet 类)打破了双亲委派,以及为什么必须打破
  4. 解决实际问题的能力:能否联系实际,解释这种设计如何避免了类库版本冲突(例如,不同 WebApp 使用不同版本的 commons-lang),以及开发、部署时的注意事项。

核心答案

Tomcat 在标准 JVM 的引导类加载器、扩展类加载器、应用类加载器之外,构建了一套自己的、具有层次结构的类加载器体系,以实现隔离与共享。其核心类加载器包括:

  1. Common ClassLoader:作为 Tomcat 自身的类加载器,其父加载器是 AppClassLoader。它负责加载 /lib 目录下,对所有 Web 应用和 Tomcat 自身都可见的、通用的类库(如 tomcat-juli.jar)。在 Tomcat 8.5 及以后,它被更细粒度地拆分,但概念类似。
  2. Catalina ClassLoader (Server ClassLoader):用于加载 Tomcat 服务器运行时自身的、内部使用的类。它与 Web 应用加载器隔离,确保容器核心不被 Web 应用影响。其父加载器通常是 Common ClassLoader
  3. Shared ClassLoader (Webapp ClassLoader):用于加载所有 Web 应用共享的类库。其父加载器是 Common ClassLoader。但注意,在 Tomcat 的默认配置中,每个 Web 应用都有自己的 WebappClassLoader,且其父加载器是 Shared ClassLoader(而非标准的父委派)。这就是实现隔离的关键。
  4. WebappClassLoader每个 Web 应用独享一个。它优先从本 Web 应用的 /WEB-INF/classes/WEB-INF/lib 目录加载类。关键点在于:它在加载类时,会首先尝试自己加载,如果找不到,才会委托给父加载器(Shared),这与标准的双亲委派顺序相反,被称为 “反向双亲委派” 或 “优先本地加载”。

深度解析

原理/机制

  1. 隔离性:每个 WebappClassLoader 实例加载的类,在其命名空间内是唯一的。WebApp A 的 WebappClassLoader 加载的 com.example.MyClass 与 WebApp B 的同名类是完全不同的,互不可见,从而实现了完美的应用隔离。
  2. 反向双亲委派WebappClassLoaderloadClass 方法重写了标准的委派逻辑。其大致流程是:
    • 检查本地已加载类的缓存。
    • 如果未加载,首先尝试使用本加载器(即从 WEB-INF 下)加载。
    • 如果本加载器找不到,且该类不属于 J2EE 规范中要求由容器父加载器加载的类(如 javax.servlet.*),则委托给父加载器(Shared -> Common -> App)。
    • 对于 javax.servlet.* 等 API 类,则直接委托给父加载器,确保所有 Web 应用使用的是容器提供的统一版本。
  3. 共享性:通过 CommonShared 加载器,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);
    }
}

最佳实践与注意事项

  1. 放置依赖:Web 应用独有的、特定版本的 jar 包,应放在 /WEB-INF/lib 下。所有 Web 应用共享的、稳定的 jar 包(如 mysql-connector-java),可考虑放在 Tomcat 的 /lib 目录(对应 Common 加载器)以减少内存占用,但要谨慎评估版本升级对所有应用的影响。
  2. 避免类泄露:Web 应用在停止或热部署时,其对应的 WebappClassLoader 实例应该被丢弃并等待 GC。但如果应用中有线程(如通过 ThreadLocal)或静态变量持有由该加载器加载的类的引用,会导致类加载器无法被回收,造成内存泄露。这是 Tomcat 热部署的常见问题。
  3. 理解 ParallelWebappClassLoader:在 Tomcat 8 及以后,默认使用 ParallelWebappClassLoader,它支持并行加载类,提升了性能。

常见误区

  1. 误以为 WebappClassLoader 完全打破了双亲委派:它只是在特定步骤(加载 Web 应用自有类时)优先处理,对于 Java 核心库和 Servlet API 等,它依然遵守委派。
  2. 混淆 reloadable 与热部署:在 context.xml 中设置 reloadable="true" 后,Tomcat 会监听 WEB-INF 下的类文件变化并触发 Web 应用重启(即销毁原 WebappClassLoader 并创建新的),这会造成 Session 丢失等副作用,并非真正的 “热替换”。生产环境通常不开启此功能。

总结

Tomcat 通过一套精心设计的、层次化的类加载器体系(特别是为每个 Web 应用创建独立的 WebappClassLoader 并采用 “反向双亲委派” 机制),完美地实现了 Web 应用之间的类隔离、Web 应用与容器的隔离,以及基础类库的共享,这是其作为多应用托管容器的基石设计。