Tomcat 中有哪些类加载器?
结论先行:Tomcat 一共定义了 5 类自有加载器(Bootstrap 由 JVM 提供):Common、Catalina(Server)、Shared、Webapp(每个应用一个)、JasperLoader(每个 JSP 一个),再加上 JVM 的 Bootstrap 与 System/App。它们按固定层级组合,默认配置下 Catalina 与 Shared 会退化合并到 Common。
1. 加载器清单
| 加载器 | 加载范围 | 配置位置 | 是否默认独立 |
|---|---|---|---|
Bootstrap | JVM 核心类(java.base) | JVM 参数 | JVM 提供 |
System / AppClassLoader | CLASSPATH:bootstrap.jar、tomcat-juli.jar | 启动脚本 | 是 |
Common | $CATALINA_HOME/lib、$CATALINA_BASE/lib | common.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 先加载了。排查顺序:
- 看异常栈里类的
getClassLoader()是哪个(在启动日志打印或jcmd查); - 检查
$CATALINA_HOME/lib是否有同名 jar(很多「神秘冲突」都源于运维往这里丢包); - 用
-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。