应复用 ScheduledExecutorService 而非每次重试新建:声明为 static final 或交由 Spring 容器管理并配置 destroyMethod="shutdown",主动 cancel 未完成任务,设置拒绝策略防止队列积压,辅以 jstack/jmap 快速定位泄漏。

这个问题很典型:每次网络超时重试都新建一个 ScheduledExecutorService,却不显式关闭,会导致线程池对象持续堆积、底层线程不释放、拒绝队列中待执行任务滞留——最终在堆或元空间(尤其是 Java 8+ 的 Metaspace)引发局部内存溢出,常见报错为 java.lang.OutOfMemoryError: Java heap space 或 java.lang.OutOfMemoryError: Metaspace。
确认是否重复创建调度器
重点检查重试逻辑中是否出现类似以下模式:
- 在方法内部直接调用
Executors.newScheduledThreadPool(n)或new ScheduledThreadPoolExecutor(n) - 没有使用
static final声明,也未从 Spring 容器等统一管理处获取实例 - 重试入口(如 catch 块、回调函数、RetryTemplate 内部)每次执行都 new 一个新调度器
定位泄漏点的实用手段
无需重启即可快速验证:
- 用
jps -l找到 Java 进程 PID,再执行jstack <pid> | grep -A5 -B5 "ScheduledThreadPoolExecutor",观察同类线程名(如pool-XX-thread-Y)数量是否随重试次数线性增长 - 执行
jmap -histo:live <pid> | grep -i "scheduledthreadpoolexecutor\|schedulede" | head -10,查看该类实例数是否异常高(几十上百即可疑) - 若启用了
-XX:+HeapDumpOnOutOfMemoryError,OOM 后用 MAT 打开 dump 文件,按 “Group by package” 查看java.util.concurrent下调度器相关类的实例数和 retained heap
修复方式必须满足三点
不是简单改个变量修饰符,而是要切断生命周期失控链:
-
复用而非新建:将调度器声明为
static final,或注入 Spring 的@Bean(destroyMethod = "shutdown")管理其生命周期 -
主动清理任务:若需动态提交任务,用
scheduler.scheduleAtFixedRate(...)后保留ScheduledFuture引用,失败时调用future.cancel(true);避免用scheduleWithFixedDelay无限递归提交 -
拒绝兜底策略:自定义
ScheduledThreadPoolExecutor,构造时传入new ThreadPoolExecutor.DiscardPolicy()或AbortPolicy,防止拒绝队列无限积压 Runnable
补充验证建议
上线前加一层防御性监控:
- 在重试方法入口记录当前调度器哈希码:
log.info("Using scheduler: {}", scheduler.hashCode());,对比日志中是否反复出现不同 hash - 用 Micrometer 注册
ThreadPoolTaskScheduler的pool.active、pool.completed指标,观察活跃线程数是否只增不减 - 对关键重试路径做单元测试,用
CountDownLatch+Thread.activeCount()断言线程数不随重试次数增长

















