慧销平台ThreadPoolExecutor内存泄漏分析
字数 1469 2025-08-11 08:35:42
ThreadPoolExecutor内存泄漏分析与解决方案
问题背景
京东生旅平台慧销系统作为平台系统对接了多条业务线,主要进行各个业务线广告、召回等活动相关内容与能力管理。系统出现以下异常现象:
- 内存持续升高,每隔2-3天会收到内存超过阈值告警
- 容器内存在24小时内持续上升
- 线程数监控显示当前线程数7427个且不断上升
问题排查步骤
1. JVM内存分析
通过监控YoungGC和FullGC情况发现:
- 存在YoungGC但没有出现FullGC
- 可能原因:
- 对象进入老年代但未达到FullGC阈值,无法回收
- 存在内存泄漏,YoungGC无法回收部分内存
2. 线程分析
通过JStack获取线程堆栈文件分析:
- 发现通过线程池创建的线程数达7000+
- 判断内存泄漏与线程池不当使用有关
问题代码分析
有问题的线程池实现
public class BackgroundWorker {
private static ThreadPoolExecutor threadPoolExecutor;
static {
init(15);
}
public static void init() {
init(15);
}
public static void init(int poolSize) {
threadPoolExecutor = new ThreadPoolExecutor(3, poolSize, 1000,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
}
public static void shutdown() {
if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) {
threadPoolExecutor.shutdownNow();
}
}
public static void submit(final Runnable task) {
if (task == null) {
return;
}
threadPoolExecutor.execute(task);
}
}
错误使用方式
public class AdActivitySyncJob {
@Scheduled(cron = "0 0/5 * * * ?")
public void execute() {
// ...
BackgroundWorker.init(40); // 每次执行都重新初始化线程池
locationCodes.forEach(locationCode -> {
showChannelMap.forEach((key,value)->{
BackgroundWorker.submit(new Runnable() {
@Override
public void run() {
// 业务逻辑
}
});
});
});
}
@PostConstruct
public void init() {
execute();
}
}
原因分析
- 线程池重复创建:每次执行
AdActivitySyncJob都会调用BackgroundWorker.init(40),创建新的线程池 - 线程池未关闭:局部创建的线程池没有被关闭,导致内存中的线程池越来越多
- 线程无法回收:
ThreadPoolExecutor在使用完成后如果不手动关闭,无法被GC回收
验证测试
public class Test {
private static ThreadPoolExecutor threadPoolExecutor;
public static void main(String[] args) {
for (int i=1;i<100;i++){
// 每次均初始化线程池
threadPoolExecutor = new ThreadPoolExecutor(3, 15, 1000,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
// 使用线程池执行任务
for(int j=0;j<10;j++){
submit(new Runnable() {
@Override
public void run() {
// 任务逻辑
}
});
}
// 获取当前所有线程
ThreadGroup group = Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
while (group != null) {
topGroup = group;
group = group.getParent();
}
int slackSize = topGroup.activeCount() * 2;
Thread[] slackThreads = new Thread[slackSize];
int actualSize = topGroup.enumerate(slackThreads);
Thread[] atualThreads = new Thread[actualSize];
System.arraycopy(slackThreads, 0, atualThreads, 0, actualSize);
System.out.println("Threads size is " + atualThreads.length);
}
}
}
输出结果:
Threads size is 302
Thread name : Reference Handler
Thread name : Finalizer
Thread name : Signal Dispatcher
Thread name : main
Thread name : Monitor Ctrl-Break
Thread name : pool-1-thread-1
Thread name : pool-1-thread-2
Thread name : pool-1-thread-3
Thread name : pool-2-thread-1
Thread name : pool-2-thread-2
Thread name : pool-2-thread-3
...
解决方案
方案1:只初始化一次,复用线程池
// 初始化一次线程池
threadPoolExecutor = new ThreadPoolExecutor(3, 15, 1000,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i=1;i<100;i++){
// 使用线程池执行任务
for(int j=0;j<10;j++){
submit(new Runnable() {
@Override
public void run() {
// 任务逻辑
}
});
}
}
输出结果:
Threads size is 8
Thread name : Reference Handler
Thread name : Finalizer
Thread name : Signal Dispatcher
Thread name : main
Thread name : Monitor Ctrl-Break
Thread name : pool-1-thread-1
Thread name : pool-1-thread-2
Thread name : pool-1-thread-3
方案2:每次执行完成后关闭线程池
对于需要频繁创建线程池的场景,应在使用后调用shutdown()方法
最终采用方案
由于BackgroundWorker定位是后台执行worker,应采用方案1,在static静态代码块中初始化,使用时无需重新初始化。
解决效果
- JVM内存监控显示内存不再持续上升
- 线程池数量恢复正常且平稳
- Jstack分析显示线程池数量恢复正常
- Dump文件分析线程池对象数量正常
技术原理深入
为什么ThreadPoolExecutor不会被GC回收
-
线程池工作原理:
execute()方法中,如果当前线程数小于核心线程数,会进入addWorker方法创建线程runWorker方法中,如果存在任务则执行,否则调用getTask()获取任务workQueue.take()会一直阻塞,等待队列中的任务
-
引用链分析:
- Thread线程一直没有结束
- 存在引用关系:
ThreadPoolExecutor->Worker->Thread - 由于存在GC ROOT的引用,所以无法被回收
线程池关闭方法
-
shutdownNow():
- 线程池拒接收新提交的任务
- 立即关闭线程池,线程池里的任务不再执行
-
shutdown():
- 线程池拒接收新提交的任务
- 等待线程池里的任务执行完毕后关闭线程池
最佳实践建议
-
线程池使用原则:
- 尽量复用线程池,避免频繁创建
- 对于长期存在的服务,使用单例线程池
- 对于短期任务,使用后必须关闭线程池
-
线程池配置建议:
- 合理设置核心线程数、最大线程数
- 根据任务特性选择合适的阻塞队列
- 设置合理的线程存活时间
- 定义恰当的拒绝策略
-
监控建议:
- 定期监控线程池状态
- 监控线程数量变化
- 监控任务队列长度
- 设置合理的告警阈值
总结
本案例展示了由于线程池不当使用导致的内存泄漏问题,通过分析我们可以得出以下关键点:
- 线程池应当作为长期资源管理,避免重复创建
- 必须正确处理线程池的生命周期,适时关闭
- 理解线程池内部工作原理有助于排查相关问题
- 合理的监控体系能及早发现问题
- 遵循线程池使用最佳实践可避免类似问题