Tomcat 可以在同一 JVM 承载多个 Web 应用。每个应用需要看到自己的类和依赖,又要安全共享 Java 与容器 API,这依靠类加载器边界实现。 热部署的本质是停止旧 Context,丢弃旧类加载器,再创建新的一套对象图。
Table of contents
Open Table of contents
为什么同名类可以共存
类的身份由“全限定类名 + 定义它的类加载器”共同决定。两个 WebApp 即使都包含 com.example.User,只要由不同加载器定义,在 JVM 看来就是不同类型。
Tomcat 的 Bootstrap、System、Common 和 WebApp 等加载层级承担不同可见范围。WebApp 通常优先加载自身类,但 Java 平台类、Servlet API 等会遵循容器规定的委派例外。 这不是简单套用一句“双亲委派”就能解释完。
Tomcat 官方类加载说明列出了加载顺序和 /WEB-INF/classes、/WEB-INF/lib
的可见性。排查版本冲突时,打印类的 getProtectionDomain() 和 getClassLoader(),不要只看 Maven 依赖树。
热部署为什么可能泄漏
旧类加载器只有在没有任何 GC Root 路径指向它或它加载的对象时才能回收。常见残留包括:
- WebApp 创建但未停止的非守护线程;
- 容器工作线程中的 ThreadLocal 值;
- 未注销的 JDBC Driver、MBean、日志监听器;
- 全局静态缓存保存应用 Class 或代理;
- 定时任务、异步回调和第三方库后台线程;
- 上下文类加载器仍指向旧 WebApp。
“应用已经停止”只表示生命周期回调执行过,不证明所有引用已经释放。
建立可验证的关闭协议
应用创建的执行器、连接池、客户端和调度器都要由明确组件持有,并在 Context 销毁时按逆序关闭。任务要响应中断,关闭要有最长等待时间,超时后记录仍存活线程和栈。
ThreadLocal 必须在 finally 中 remove。驱动、MBean 和监听器使用注册句柄对称注销。不要依赖 finalize 或“等 GC 自己处理外部资源”。
用证据确认类加载器泄漏
在可控环境反复重新部署多次,执行 Full GC 后比较类加载器数量和 Metaspace 基线。堆转储中从旧 WebappClassLoaderBase 查找 GC Root 路径,结合线程转储确认后台线程。
一次部署后的 Metaspace 增长可能是正常加载;每次部署都保留近似同样一批旧类,才是强烈泄漏信号。修复后重复相同步骤,确认旧加载器数量回落。
生产发布建议
开发环境可用热部署提高反馈速度;生产环境更适合不可变实例滚动替换,隔离旧进程并让操作系统最终回收资源。即使如此,优雅停机仍必须等待在途请求并停止后台任务。
下一步
阅读HTTP/2、HTTP/3、WebSocket 与 gRPC 选型,把传输能力映射到真实交互需求。