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 核心线程默认永不超时

最终导致:

线程池泄漏

相关推荐
JAVA96510 分钟前
JAVA面试-JVM篇 02-G1垃圾收集器的工作原理是什么与CMS的区别
java·jvm·面试
Javatutouhouduan29 分钟前
深入学习JVM底层原理:源码剖析与实例详解!
java·jvm·java面试·后端开发·java程序员·java八股文·java性能优化
宸丶一18 小时前
Day 13:持久化记忆 - 让 Agent 拥有长期记忆
jvm·python·ai
cfm_291420 小时前
JVM新一代垃圾收集器深度解析:G1与ZGC
java·jvm
顺风尿一寸1 天前
JVM 字段布局揭秘:Best‑Fit 算法如何为每个字段精准分配偏移量
jvm
小bo波1 天前
Java反射机制——运行时"透视"类的秘密
java·jvm·反射·源码分析·动态代理·进阶·spring底层·框架原理
程序猿阿伟1 天前
《拆解Chrome存储架构:浏览痕迹的残留死角与清除路径》
jvm·chrome·架构
于指尖飞舞1 天前
java后端面试题(jvm极简)
java·开发语言·jvm
鹅城剑仙1 天前
JVM 内存模型与 GC 调优实战指南
jvm
Javatutouhouduan1 天前
2026年Java面试核心讲(终极版)全网首次开源!
java·jvm·java多线程·java面试·后端开发·java程序员·java八股文