Java线程泄漏案例

wen java案例 2

本文目录导读:

Java线程泄漏案例

  1. 案例一:ThreadLocal 未清理导致线程池中的线程“僵尸化”
  2. 案例二:new Thread() 未受控创建
  3. 案例三:线程池的 corePoolSize 设置过大且使用 SynchronousQueue 无界
  4. 案例四:使用 CompletableFuture + ForkJoinPool 不当
  5. 通用排查工具与步骤
  6. Java线程泄漏的本质

Java线程泄漏是生产环境中一种棘手的内存问题,因为线程本身占用大量JVM堆外内存(线程栈),一旦泄漏,最终会导致 OutOfMemoryError: unable to create new native thread

下面通过几个典型的实战案例,帮你认识线程泄漏的成因、排查方法及修复方案。


ThreadLocal 未清理导致线程池中的线程“僵尸化”

问题描述

某系统使用固定线程池(Executors.newFixedThreadPool(100))处理业务,运行几天后频繁出现以下错误:

java.lang.OutOfMemoryError: unable to create new native thread

根因分析

代码中使用 ThreadLocal 存储用户上下文信息(如用户ID、角色),但在线程池中的线程复用场景下,任务执行完毕后没有调用 remove() 清理

  • 线程池中的线程是长期存在的,不会被回收。
  • ThreadLocal 中的 value 会被线程持续持有,形成 线程 → ThreadLocalMap → value 的强引用链。
  • 尽管堆内存(Heap)看起来还在增长,但真正的致命点是:每次新的任务都可能往 ThreadLocal 里塞入新的对象,导致该线程持有的对象越来越多。

演进后果

JVM的堆内存耗尽,尝试创建新的线程时,操作系统无法分配原生线程栈,抛出 unable to create new native thread

修复方案

// 错误示例:只设置不清理
try {
    UserContext.set(user);
    doTask();
} finally {
    // 修复:必须清理,防止线程复用后数据残留
    UserContext.remove();
}

new Thread() 未受控创建

问题描述

某订单推送系统,每来一条订单就 new Thread(() -> pushOrder(order)).start(),高并发下系统崩溃。

根因分析

  • 每个 new Thread() 默认分配 1MB 左右的线程栈空间(JVM参数 -Xss 决定)。
  • 如果推送服务响应慢(比如下游 HTTP 超时),线程阻塞在 I/O 上无法释放。
  • 短时间内大量线程堆积,占用大量原生内存,最终触发 OutOfMemoryError: unable to create new native thread

排查方法

用以下命令查看线程数:

jstack <pid> | grep "java.lang.Thread.State" | awk '{print $2}' | sort | uniq -c | sort -rn

如果看到大量 RUNNABLEWAITING 状态的推送线程,且线程ID在持续增长,基本可以确认。

修复方案

使用有界线程池 + 拒绝策略

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
pool.execute(() -> pushOrder(order));

线程池的 corePoolSize 设置过大且使用 SynchronousQueue 无界

问题描述

某系统配置如下:

new ThreadPoolExecutor(
    0, Integer.MAX_VALUE,  // 核心线程0,最大线程无界
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>()  // 不缓存任务,直接创建线程
);

根因分析

  • SynchronousQueue 不会缓存任务,每一个任务到达时,若没有空闲线程,就会新建线程。
  • 当任务提交速率大于任务执行速率时,线程数无限增长。
  • 最终线程数达到数千或数万,每个线程占用约 1MB 栈空间,内存耗尽。

修复方案

核心思想:有界队列 + 固定上限的线程数

new ThreadPoolExecutor(
    8, 32,  // 核心线程8,最大32
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)  // 有界队列
);

使用 CompletableFuture + ForkJoinPool 不当

问题描述

某数据分析系统大量使用:

CompletableFuture.runAsync(() -> process(data));

默认使用 ForkJoinPool.commonPool(),其线程数由 Runtime.getRuntime().availableProcessors() - 1 决定。

根因分析

  • process(data) 是阻塞操作(如数据库查询),大量阻塞任务会让 commonPool 的线程全部阻塞。
  • 后续任务不断提交,commonPool 会自动从 0 扩展到最大 32767 线程(理论值),同样造成线程爆炸。
  • ForkJoinPool 默认不会拒绝任务,会无限扩张。

修复方案

指定专用线程池,不要复用 commonPool

ExecutorService customPool = new ThreadPoolExecutor(
    16, 16,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<>(10000),
    new NamedThreadFactory("analysis-")
);
CompletableFuture.runAsync(() -> process(data), customPool);

通用排查工具与步骤

确认是否线程泄漏

# 查看进程的线程数(多次执行看是否持续增长)
watch -n 1 "ls /proc/<pid>/task | wc -l"

抓取线程快照

jstack -l <pid> > thread_dump_1.txt
sleep 5
jstack -l <pid> > thread_dump_2.txt

对比两份dump,重点观察:

  • 线程名是否一致(比如有没有线程名重复?)
  • 是否有大量相同状态(如 WAITING on monitor)的线程
  • 是否有线程只增不减

分析堆和原生内存

# 查看堆内存使用
jmap -heap <pid>
# 查看线程栈占用的内存(Linux)
cat /proc/<pid>/status | grep Threads

Java线程泄漏的本质

场景 本质原因 修复思路
ThreadLocal 未清理 线程复用导致对象积累 在 finally 中调用 remove()
new Thread() 无限创建 没有线程复用机制 使用固定线程池
线程池最大线程数无界 任务队列饱和导致线程扩张 设置最大线程数 + 有界队列
ForkJoinPool.commonPool 被阻塞 默认线程池被滥用 使用业务隔离的专用线程池

核心修复黄金法则:

  1. 尽量用线程池,不要裸奔 new Thread()
  2. 线程池必须配置有界队列拒绝对策
  3. ThreadLocal 执行完必须 remove()
  4. 监控线程数和核心线程活跃数,设置告警。

抱歉,评论功能暂时关闭!