JVM线程泄漏 问题记录

一、问题现象

系统突然出现:

text 复制代码
java.lang.OutOfMemoryError: unable to create native thread

并伴随:

text 复制代码
pool-3-thread-1375
pool-3-thread-1383
pool-3-thread-1385

等大量线程。

同时 MQTT 连接异常:

text 复制代码
Client is not connected (32104)

二、问题分析过程

1. ScheduledExecutorService 是什么?

业务代码:

java 复制代码
private final ScheduledExecutorService scheduler1 =
        Executors.newScheduledThreadPool(4);

private final ScheduledExecutorService scheduler =
        Executors.newSingleThreadScheduledExecutor();

分析:

newScheduledThreadPool(4)

表示:

  • 创建一个支持"定时任务"的线程池
  • 核心线程数 = 4
  • 最多可以同时运行4个任务

适用于:

  • 定时任务
  • 延迟任务
  • 周期任务

newSingleThreadScheduledExecutor()

表示:

  • 创建一个只有1个线程的定时线程池
  • 所有任务串行执行

适用于:

  • 顺序执行
  • 单线程定时控制

三、为什么线程会爆炸?

用户原有线程池:

java 复制代码
private static final ThreadPoolExecutor THREAD_EXECUTOR =
    new ThreadPoolExecutor(
        ThreadConfig.CORE_POOL_SIZE,
        ThreadConfig.MAX_POOL_SIZE,
        ThreadConfig.KEEP_ALIVE_SECONDS,
        TimeUnit.SECONDS,
        new SynchronousQueue<>(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

配置:

java 复制代码
CORE_POOL_SIZE = 50
MAX_POOL_SIZE = 200

但系统实际线程数:

text 复制代码
1300+

用户疑问:

"不是最大200吗?"


四、真正原因

不是线程池不释放

而是:

系统中疯狂创建了新的线程池

jstack 中发现:

text 复制代码
pool-779-thread-1
pool-1120-thread-1
pool-2301-thread-1
pool-2798-thread-1

这说明:

  • JVM 生命周期里
  • 至少创建了 2798 个线程池

五、真正问题代码

用户怀疑代码:

java 复制代码
private final ScheduledExecutorService scheduler =
        Executors.newSingleThreadScheduledExecutor();

类:

java 复制代码
public class Shower

不是 Spring 单例。

意味着:

java 复制代码
new Shower()

每次都会:

text 复制代码
创建一个新的 scheduler

而:

java 复制代码
newSingleThreadScheduledExecutor()

会:

  • 创建永久核心线程
  • 默认不会自动释放

六、为什么 shutdown 没生效?

业务代码:

java 复制代码
public void shutdown() {
    if (!scheduler.isShutdown()) {
        scheduler.shutdown();

        if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
            scheduler.shutdownNow();
        }
    }
}

这个 shutdown 本身没问题。

真正问题:

很多情况下:

java 复制代码
mqttFuture.get()

阻塞。

导致:

text 复制代码
后面的 shutdown 根本没机会执行

于是:

scheduler 永久残留。


七、为什么运行6天后才爆?

因为:

不是瞬间泄漏。

而是:

慢性线程池泄漏

例如:

每天 5000 请求。

即使:

只有 0.1% 的任务:

text 复制代码
没有成功 shutdown

长期运行后:

线程池数量仍然会越来越大。

最终:

text 复制代码
unable to create native thread

八、jstack 的核心作用

命令:

bash 复制代码
jstack PID > jstack.log

作用:

  • 查看当前线程状态
  • 判断线程卡在哪里
  • 判断线程池是否泄漏

注意:

jstack 不是历史日志

它只是:

当前瞬间的线程快照


重启后会消失吗?

会。

因为:

  • JVM 已重启
  • 线程已重新创建
  • 之前线程现场不存在了

所以:

出问题时必须立即抓 jstack


九、正确的线程池架构

错误:

java 复制代码
每个任务 new scheduler

正确:

java 复制代码
private static final ScheduledExecutorService SCHEDULER =
    new ScheduledThreadPoolExecutor(
        8,
        new ThreadFactoryBuilder()
            .setNameFormat("a-%d")
            .build()
    );

核心思想:

全局共享线程池

而不是:

text 复制代码
每个任务创建线程池

十、scheduler 的真正职责

scheduler:

本质上:

只是时间调度器

它负责:

  1. 接收 schedule 任务
  2. 到时间后触发任务

其余时间:

text 复制代码
WAITING

属于正常状态。


十一、正确职责拆分

scheduler

只负责:

text 复制代码
什么时候执行

例如:

java 复制代码
scheduler.schedule(() -> {
    mqttExecutor.submit(() -> {
        doBusiness();
    });
}, 10, TimeUnit.MINUTES);

MQTT_EXECUTOR

负责:

text 复制代码
真正业务执行

例如:

  • MQTT publish
  • Redis
  • DB
  • HTTP

十二、为什么 scheduler 线程不能阻塞?

用户之前:

java 复制代码
scheduler.schedule(() -> {
    mqttFuture.get();
});

这会导致:

text 复制代码
scheduler线程被业务阻塞

例如:

100个淋浴同时触发。

8个线程全部:

text 复制代码
等待 mqttFuture.get()

后续任务只能排队。


十三、推荐最终架构

scheduler

java 复制代码
newScheduledThreadPool(8)

只负责调度。


MQTT_EXECUTOR

java 复制代码
16~32线程

负责业务执行。


future

必须:

java 复制代码
orTimeout()

或者:

java 复制代码
get(timeout)

避免永久等待。


十四、最佳异步方案

不推荐:

java 复制代码
future.get()

推荐:

java 复制代码
future.whenComplete(...)

真正全异步。

例如:

java 复制代码
taskSupplier.get()
    .orTimeout(10, TimeUnit.SECONDS)
    .whenComplete((result, ex) -> {

        if (ex != null) {
            handleFailure();
            return;
        }

        if (Boolean.TRUE.equals(result)) {
            handleSuccess();
        } else {
            handleFailure();
        }
    });

十五、最终问题根因总结

真正导致:

text 复制代码
unable to create native thread

的原因:

不是 JVM 不释放线程。

而是:

动态创建 ScheduledExecutorService

部分异常路径没有 shutdown

newSingleThreadScheduledExecutor 核心线程默认永不超时

最终导致:

线程池泄漏

相关推荐
驭渊的小故事1 天前
多线程01(线程状态和线程的sleep,线程终止(Interrupt)的小关联)
java·jvm·算法
深蓝轨迹1 天前
深入解析JVM方法区与StringTable机制
jvm·jdk·方法区·java八股
Dicky-_-zhang1 天前
分布式锁实战:Redis与ZooKeeper对比选型与实现方案
java·jvm
深蓝轨迹1 天前
JVM 垃圾回收器详解:Serial、Parallel、CMS 与 G1 的原理与实践
jvm·垃圾回收·gc调优
自律懒人1 天前
阿里Qoder 1.0实测:对比Cursor和Claude Code,国产AI编程工具做到哪一步了?
jvm·深度学习·ai编程
高级c1 天前
10分钟上手昇腾 NPU 算子开发入门与实战
java·jvm·spring
没文化的阿浩1 天前
【Linux系统】线程的同步与互斥(1)——互斥量mutex
linux·运维·jvm
深蓝轨迹1 天前
JVM 类加载机制详解(生命周期・双亲委派・自定义加载器)
jvm·类加载器·双亲委派