Tomcat 9.0.35移Lib至SharedFolder后堆内存升高原因问询
问题分析与解答
首先,我们拆解你的场景:将Web应用依赖从WEB-INF/lib移至Tomcat的Shared Folder,且设置delegate="true"(类加载优先级:Common → Shared → WebApp),结果启动耗时缩短(符合预期,类加载提前完成,无需每个Web应用重复加载),但堆内存从800MB升至1.1GB,且Finalizer占用大幅增长。下面逐一分析核心原因:
1. 堆内存上升的核心逻辑:类加载器生命周期与对象持有差异
当依赖库由Shared类加载器加载时,这些类的元数据、静态对象、全局实例会绑定到Tomcat的生命周期(而非单个Web应用)。而原来在WEB-INF/lib时,这些资源属于Web应用类加载器,仅在Web应用运行期间被持有,部分闲置对象可能在启动完成后被GC回收。
具体可能的场景:
- 部分依赖库在类加载时会初始化全局静态对象池(比如连接池、缓存实例),由Shared类加载器加载时,这些池会被初始化一次并被Tomcat全局持有,不会随Web应用状态变化被回收;而在WebApp类加载器下,这些池仅为当前Web应用服务,启动后部分闲置资源会被GC。
- Shared类加载器加载的Class对象会被Tomcat核心类加载器引用,类元数据(方法区/元空间内容)无法被GC,而WebApp类加载器的类元数据在Web应用卸载时可被回收——不过你观察到的是启动后的堆内存变化,更多是实例对象的持有差异导致的。
2. Finalizer占用大幅增长的关键原因
Finalizer队列增长的本质是带有finalize()方法的对象无法及时被GC回收,这和Shared类加载器的特性直接相关:
- 依赖库类由Shared加载时,其创建的实例可能被Tomcat全局组件(比如连接器、全局资源管理器)意外持有引用。例如,某个数据库连接池类在Shared加载后,连接实例被Tomcat全局上下文持有,导致这些实例无法被标记为可回收,只能留在Finalizer队列等待处理。
- 部分库在WebApp类加载器下时,
finalize()方法的触发逻辑和Web应用绑定,启动完成后闲置资源对象会被及时回收;但在Shared类加载器下,对象生命周期被延长,导致Finalizer队列堆积。
3. 是否存在问题?
需分情况判断:
- 如果堆内存启动后稳定在1.1GB,无持续增长,且GC频率正常,这属于类加载器切换后的正常现象——Shared加载的资源是全局复用的,内存占用是Tomcat级别的,而非单个Web应用。
- 如果堆内存持续增长,或Finalizer队列一直堆积导致OOM,那就是问题了:可能是依赖库存在内存泄漏(比如静态集合持有对象引用),或Tomcat全局配置与依赖库不兼容(比如某些带有Web应用上下文依赖的框架不适合在Shared Folder中加载)。
建议排查步骤
- 监控堆内存长期变化:使用
jstat或JConsole观察GC后的堆内存是否稳定,若GC后内存回落,说明是启动阶段临时对象,无需担心。 - 分析Finalizer对象类型:使用
jmap -histo:live <pid>查看存活对象中数量最多的类,尤其是带有finalize()方法的类,定位具体依赖库。 - 尝试部分依赖回移:若某个特定库移至Shared后导致Finalizer暴涨,说明该库不适合全局加载,应保留在
WEB-INF/lib中。
内容的提问来源于stack exchange,提问作者André Luís Oliveira
相关产品推荐
相关产品推荐

