Tomcat 类加载器源码分析
概述
上一轮从概念上认识了 Tomcat 的类加载器层次,本文深入源码:WebappClassLoaderBase 如何重写 loadClass 实现"打破双亲委派"、本地优先如何落地、JSP 类如何被处理、以及类加载器的关闭与卸载机制。这是理解 Tomcat 多应用隔离和内存泄漏问题的关键代码。
一、类加载器继承体系
java.lang.ClassLoader
└── URLClassLoader
└── WebappClassLoaderBase(抽象基类,含全部核心逻辑)
├── WebappClassLoader(标准实现)
└── ParallelWebappClassLoader(并行加载,默认)// org.apache.catalina.loader.WebappClassLoaderBase
public abstract class WebappClassLoaderBase extends URLClassLoader {
protected final WebResourceRoot resources; // WEB-INF/classes 与 lib 的资源根
protected final ClassLoader parent; // 父加载器(Shared/Common)
...
}关键设计:所有核心逻辑在基类,子类只是配合 JVM 的并行类加载锁(ParallelWebappClassLoader 允许不同类并行加载,避免锁竞争)。
二、loadClass:打破双亲委派的核心
2.1 完整加载流程
// WebappClassLoaderBase.loadClass() —— 核心逻辑
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// ===== 阶段一:查缓存 =====
clazz = findLoadedClass0(name); // 1) 本加载器已加载缓存
if (clazz == null) {
clazz = findLoadedClass(name); // 2) JVM 全局已加载缓存
}
// ===== 阶段二:系统包过滤 =====
if (clazz == null) {
// 过滤器:javax.* / java.* / org.apache.catalina.* 等必须父加载
String packageName = getPackageName(name);
if (packageName != null && isPackageProtected(packageName)) {
// 安全包 → 直接交给父加载器(不可被应用覆盖)
clazz = parent.loadClass(name, false);
}
}
// ===== 阶段三:本地优先(打破双亲委派的关键) =====
if (clazz == null) {
try {
clazz = findClass(name); // 先找 WEB-INF/classes 与 WEB-INF/lib
} catch (ClassNotFoundException e) {
// 忽略,继续
}
}
// ===== 阶段四:兜底委托父加载器 =====
if (clazz == null) {
clazz = parent.loadClass(name, false);
}
// ===== 阶段五:resolve =====
if (resolve && clazz != null) {
resolveClass(clazz);
}
return clazz;
}
}2.2 与默认双亲委派的差异
| 步骤 | 默认 ClassLoader | WebappClassLoaderBase |
|---|---|---|
| 缓存检查 | 有 | 有 |
| 委派父类 | 第一步 | 仅对系统包 / 兜底 |
| 本地加载 | 父类失败后 | 优先执行 |
| 系统包保护 | 无 | 有(javax.* 等) |
一句话概括实现:"系统包父优先,应用包本地优先,都不行再委派"。
三、findClass:本地类查找
3.1 实现逻辑
// WebappClassLoaderBase.findClass()
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 查本地资源(WEB-INF/classes → WEB-INF/lib/*.jar)
WebResource resource = resources.getClassLoaderResource(name);
if (resource.isFile()) {
// 2. 读取字节码
byte[] classBytes = resource.getContent();
// 3. 定义类
return defineClass(name, classBytes, 0, classBytes.length);
}
// 4. 找不到 → 抛 ClassNotFoundException(由 loadClass 的兜底阶段接管)
throw new ClassNotFoundException(name);
}WebResourceRoot 维护资源查找顺序,getClassLoaderResource 的目录优先级:
1. /WEB-INF/classes ← 应用类(最高优先级)
2. /WEB-INF/lib/*.jar ← 应用依赖 Jar
3. 父加载器兜底(由 loadClass 处理)3.2 资源查找:getResourceAsStream
// 与类查找对称的资源查找
public URL findResource(String name) {
// 先本地资源,再委托父类(用于读取 properties、xml 等)
WebResource resource = resources.getResource("/" + name);
if (resource.isFile()) {
return resource.getURL();
}
return super.findResource(name);
}类与资源查找顺序一致:应用内的配置优先,避免被容器资源覆盖。
四、包保护机制:isPackageProtected
4.1 保护列表来源
// 包保护判断
protected boolean isPackageProtected(String packageName) {
// 读取 catalina.properties 的 package.definition
// 常见受保护前缀:javax.、java.、org.apache.catalina. 等
if (protectedPackages != null) {
for (String p : protectedPackages) {
if (packageName.startsWith(p)) return true;
}
}
return false;
}4.2 为什么要保护
如果应用在 WEB-INF/lib 里放置了自己的 javax.servlet-api 并成功加载,会出现:
- 容器与应用各有一份
HttpServletRequest,类型不互通 - 过滤器/监听器用应用版接口实现,容器传容器版实例 →
ClassCastException
保护机制从源头杜绝了这种类分裂。
五、JSP 类的加载
JSP 编译产物由 Jasper 放入 work/Catalina/<host>/<app>/org/apache/jsp/,由同一个 WebappClassLoader 加载:
// JspServlet / JspRuntimeContext 触发编译后
// 编译生成的 Servlet 类通过 context classloader 加载
Class<?> servletClass = cl.loader.loadClass(className);流程:
JSP 文件
→ Jasper 编译(生成 .java → javac → .class)
→ 写入 work 目录
→ WebappClassLoader 加载编译类
→ 作为普通 Servlet 执行JSP 更新检测:JasperManager 比较 JSP 文件时间戳与编译产物,变化则触发重新编译,旧类被丢弃——这依赖 WebappClassLoader 能释放旧类。
六、类卸载与内存泄漏
6.1 卸载机制
Java 类无法显式卸载,只能依赖 类加载器被 GC 回收。Context 停止时:
// WebappClassLoaderBase.stop() / close()
public void stop() throws LifecycleException {
// 1. 清除缓存
clearReferences(); // 清理线程、ThreadLocal、静态引用等
// 2. 释放资源
resources.stop();
...
}clearReferences() 是防泄漏的关键:扫描并清理
- 持有本加载器类的 ThreadLocal 值
- 运行中的线程(
Threads检查) - 静态字段引用(通过
ObjectStreamClass等手段尽力而为)
6.2 泄漏的本质
Context.stop()
└─ WebappClassLoader 引用被外部持有(静态集合/长生命周期线程)
└─ 类 → 常量池 → ClassLoader → 永不回收排查思路:dump 堆找 WebappClassLoader 实例的引用链,定位持有者。
七、WebappClassLoader 与 JDBC 驱动的特殊情况
JDBC 驱动注册进 DriverManager 是静态注册,驱动类若由 Webapp 加载,Context 卸载时驱动仍在 DriverManager 中(由系统加载器持有)→ 类泄漏。经典解法:
- 驱动放入
$CATALINA_HOME/lib(Common 加载器),应用卸载不影响 - 或 Context 关闭时显式
DriverManager.deregisterDriver()
八、对比总结
| 维度 | 传统双亲委派 | WebappClassLoader |
|---|---|---|
| 查找顺序 | 父 → 子 | 缓存 → 系统包父 → 本地 → 父 |
| 隔离性 | 无(全 JVM 共享) | 每应用独立 |
| 类型安全 | — | 系统包保护防止类分裂 |
| 卸载 | 不可控 | Context 关闭后整体可回收 |
| 泄漏风险 | 低 | 高(引用持有导致) |
九、调试方法
| 手段 | 用途 |
|---|---|
-verbose:class | 观察类由哪个加载器加载 |
| jmap 堆 dump | 定位 WebappClassLoader 引用链 |
jmap -clstats <pid> | 各加载器加载类数量统计 |
| Manager 应用 | 查看每个应用的类加载器层级 |
| Psi-Probe | 可视化类加载器树 |
参考链接: