Tomcat 中有哪些类加载器?

#Java#面试八股

Tomcat 中有哪些类加载器?

结论先行:Tomcat 一共定义了 5 类自有加载器(Bootstrap 由 JVM 提供):Common、Catalina(Server)、Shared、Webapp(每个应用一个)、JasperLoader(每个 JSP 一个),再加上 JVM 的 Bootstrap 与 System/App。它们按固定层级组合,默认配置下 Catalina 与 Shared 会退化合并到 Common。

1. 加载器清单

加载器加载范围配置位置是否默认独立
BootstrapJVM 核心类(java.base)JVM 参数JVM 提供
System / AppClassLoaderCLASSPATH:bootstrap.jar、tomcat-juli.jar启动脚本是
Common$CATALINA_HOME/lib、$CATALINA_BASE/libcommon.loader是
Catalina(Server)Tomcat 内部组件专用server.loader否(空则复用 Common)
Shared所有 webapp 共享、Tomcat 自身不用shared.loader否(空则复用 Common)
Webapp单个应用的 WEB-INF/classes、WEB-INF/lib每应用一个是
JasperLoader单个 JSP 编译出的 Servlet 类每 JSP 一个是(JSP 场景)

2. 层级关系

Bootstrap(JVM)
  └─ System(AppClassLoader)
       └─ Common ──┬── Catalina(server.loader,默认空 → 即 Common)
                   └── Shared(shared.loader,默认空 → 即 Common)
                        └─ Webapp(app1)   ← 每个应用独立
                        │    └─ JasperLoader(JSP)
                        └─ Webapp(app2)
                             └─ JasperLoader(JSP)

server.loader 与 shared.loader 为空时,Tomcat 只创建一个 Common 实例,Catalina 与 Shared 都指向它——这是绝大多数部署的实际形态。

3. 各加载器的职责

  • Common:放 Tomcat 与应用都需要的类,典型是 JDBC 驱动、日志实现。注意:放进来就意味着所有应用共享同一个版本。
  • Catalina:只给 Tomcat 自身用,应用看不到。用于隔离「容器依赖」与「应用依赖」,避免应用升级 jar 影响容器。
  • Shared:所有应用共享、但容器不需要(如公共工具包)。默认不启用。
  • Webapp:应用私有类,先自己找再委派,保证应用间隔离。
  • JasperLoader:JSP 编译产物,支持修改 JSP 后重新加载。

4. 配置

# conf/catalina.properties

# ① 公共库:容器 + 所有应用可见
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

# ② 仅容器可见(放开后需自行确保 Tomcat 依赖完整)
server.loader=${catalina.base}/server/lib/*.jar

# ③ 所有应用共享(容器不可见)
shared.loader=${catalina.base}/shared/lib/*.jar
<!-- conf/context.xml:全局默认,可被应用的 META-INF/context.xml 覆盖 -->
<Context>
    <!-- 应用是否允许覆盖共享库中的类:false(默认)表示先本地 -->
    <Loader delegate="false" />
</Context>

5. 版本冲突怎么排查

java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument

这类错误的典型原因是:应用 WEB-INF/lib 里是 guava 30,而 $CATALINA_HOME/lib 里放了 guava 20,且某个类被 Common 先加载了。排查顺序:

  1. 看异常栈里类的 getClassLoader() 是哪个(在启动日志打印或 jcmd 查);
  2. 检查 $CATALINA_HOME/lib 是否有同名 jar(很多「神秘冲突」都源于运维往这里丢包);
  3. 用 -verbose:class 或 jmap -clstats 确认加载来源。

6. 常见追问

追问一:为什么 server.loader 和 shared.loader 默认是空的? Tomcat 认为大多数场景不需要这么细的隔离,合并到 Common 更简单、类加载次数更少。需要严格隔离容器依赖时才显式配置。

追问二:WEB-INF/lib 的 jar 会被容器加载吗? 不会。它只属于该应用的 WebappClassLoader。容器(Catalina)看不到应用类,这也是「应用类不能反向影响容器」的保证。

追问三:多个应用能共享一个 jar 又互不干扰吗? 不能同时满足。共享(放 Common/Shared)意味着同一个 Class 实例、同一个版本;隔离(放各自 WEB-INF/lib)意味着各加载各的。想要「共享类但版本不同」只能靠 OSGi 那类模块化方案。

追问四:Tomcat 的加载器会泄漏吗? 会。应用重部署时若线程、ThreadLocal、DriverManager 仍持有旧 WebappClassLoader 的引用,整个加载器无法回收,Metaspace 持续增长。需要在 ServletContextListener 里主动清理。

追问五:Spring Boot 内嵌 Tomcat 用哪几个加载器? 只有 JVM 的 Bootstrap + AppClassLoader(fat jar 由 LaunchedURLClassLoader 加载)。Tomcat 的 Common/Webapp 体系只存在于独立部署(WAR)模式。

小结

记住「默认三层、可扩五层」:Bootstrap → System → Common → Webapp(+ Jasper),需要更细隔离时再打开 server.loader/shared.loader。每加一层隔离,就多一份「类到底从哪加载」的排查成本,所以生产上宁可少放共享 jar。

笔记加载中…