Tomcat卸载WAR包后未卸载类,疑引发元空间内存泄漏
分析与解决方案:Tomcat 7 应用卸载后 Metaspace 暴涨、类加载过多问题
首先明确一点:正常情况下,Tomcat 卸载 Web 应用时,对应的WebappClassLoader应该被垃圾回收,它加载的类也会随之被卸载,Metaspace 占用应该相应下降。但从你的描述来看——多次部署/卸载后 Metaspace 超 3GB、加载70万个类(远超正常2万),再加上catalina.log里的线程停止失败、疑似内存泄漏日志,这几乎可以肯定是类加载器内存泄漏导致的。
下面拆解问题原因和对应的排查、解决方向:
核心原因分析
类加载器无法被GC回收是Metaspace暴涨的根因,而导致类加载器被持有引用的常见场景包括:
1. 未正确停止的自定义线程
这是你日志里明确提到的“线程停止失败”对应的最可能原因:
- 如果你的应用启动了自定义线程(比如定时任务线程、业务处理线程),但没有设置为守护线程,或者在应用卸载时没有主动中断、关闭这些线程,这些线程会一直存活在JVM中。
- 线程对象本身会持有创建它的
WebappClassLoader引用,只要线程不终止,类加载器就无法被GC,它加载的所有类也会一直占用Metaspace。
2. 静态变量/静态集合的泄漏
- 应用中的静态变量(尤其是静态集合,比如
static Map、static List)如果持有了大量应用内对象的引用,这些静态变量属于类,而类又绑定到WebappClassLoader上。 - 如果外部组件(比如Tomcat的核心线程、其他未卸载的应用)意外引用了这些静态变量,就会间接持有类加载器的引用,导致无法回收。
3. ThreadLocal 未清理
- 很多应用会用ThreadLocal存储请求上下文,但如果在请求结束后没有调用
ThreadLocal.remove(),这些Entry会留在线程的ThreadLocalMap中。 - Tomcat的线程池会复用线程,这些残留的Entry会持有应用类加载器的引用,导致类加载器无法被GC,进而类无法卸载。
4. 第三方库或JDBC驱动的问题
- 一些老旧的第三方库可能存在内存泄漏,比如内部缓存未清理、静态引用未释放;
- 如果你的应用直接注册JDBC驱动(而不是使用Tomcat自带的数据源),驱动会被
BootstrapClassLoader加载,同时它会持有应用类加载器的引用,导致类加载器无法回收。
具体解决步骤
1. 优先处理线程问题
- 检查应用中所有自定义线程:将非必须的线程设置为守护线程(
thread.setDaemon(true)),这样JVM退出时会自动终止这些线程; - 在
ServletContextListener的contextDestroyed方法中,显式停止所有自定义线程:调用thread.interrupt(),同时在线程的run方法中处理中断信号(比如检查Thread.currentThread().isInterrupted()),确保线程能正常退出。
2. 排查静态资源泄漏
- 梳理应用中的静态变量,尤其是静态集合,确保在应用卸载时(比如
contextDestroyed方法中)清空这些集合,释放持有的对象引用; - 避免在静态变量中持有应用内的对象实例,尽量使用弱引用(
WeakReference)来存储非必须的对象。
3. 清理ThreadLocal引用
- 确保所有ThreadLocal在使用完毕后调用
remove(),比如在过滤器(Filter)的doFilter方法末尾,或者在Controller的请求处理完成后清理; - 如果使用了框架提供的ThreadLocal(比如Spring的RequestContextHolder),确保框架会自动清理,或者在应用卸载时手动调用清理方法。
4. 调整JVM和Tomcat配置辅助排查
- 添加JVM参数跟踪类加载/卸载:
-XX:+TraceClassLoading -XX:+TraceClassUnloading,启动Tomcat后查看日志,找出哪些类反复加载但未被卸载; - 开启Tomcat的内存泄漏检测:在
conf/context.xml中添加监听器配置,帮助自动处理部分常见泄漏:<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/> <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
5. 优化JDBC配置
- 放弃在应用中直接注册JDBC驱动,改用Tomcat自带的数据源(比如
org.apache.tomcat.jdbc.pool.DataSource),在conf/context.xml中配置数据源,应用通过JNDI获取连接,避免驱动持有类加载器引用。
6. 考虑升级Tomcat版本
你使用的Tomcat 7.0.62是比较旧的版本(发布于2015年),后续的Tomcat 7.0.x版本修复了不少内存泄漏相关的bug(比如线程池处理、类加载器回收逻辑)。升级到Tomcat 7.0的最新稳定版,或者直接升级到Tomcat 8.5/9.x(如果应用兼容的话),能有效减少这类问题的发生。
内容的提问来源于stack exchange,提问作者redbeard1970
相关产品推荐
相关产品推荐

