跳到正文
Elaine Blog
返回

Tomcat 类加载、热部署与内存泄漏

网络与 I/O

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 路径指向它或它加载的对象时才能回收。常见残留包括:

“应用已经停止”只表示生命周期回调执行过,不证明所有引用已经释放。

建立可验证的关闭协议

应用创建的执行器、连接池、客户端和调度器都要由明确组件持有,并在 Context 销毁时按逆序关闭。任务要响应中断,关闭要有最长等待时间,超时后记录仍存活线程和栈。

ThreadLocal 必须在 finallyremove。驱动、MBean 和监听器使用注册句柄对称注销。不要依赖 finalize 或“等 GC 自己处理外部资源”。

用证据确认类加载器泄漏

在可控环境反复重新部署多次,执行 Full GC 后比较类加载器数量和 Metaspace 基线。堆转储中从旧 WebappClassLoaderBase 查找 GC Root 路径,结合线程转储确认后台线程。

一次部署后的 Metaspace 增长可能是正常加载;每次部署都保留近似同样一批旧类,才是强烈泄漏信号。修复后重复相同步骤,确认旧加载器数量回落。

生产发布建议

开发环境可用热部署提高反馈速度;生产环境更适合不可变实例滚动替换,隔离旧进程并让操作系统最终回收资源。即使如此,优雅停机仍必须等待在途请求并停止后台任务。

下一步

阅读HTTP/2、HTTP/3、WebSocket 与 gRPC 选型,把传输能力映射到真实交互需求。


分享这篇文章:

上一篇
Tomcat 线程模型与性能调优:从容量模型开始
下一篇
HTTP/2、HTTP/3、WebSocket 与 gRPC 选型