Java线程泄漏
什么是线程泄露?
线程泄露是指:应用程序中创建了线程,但在其任务完成后却无法被垃圾回收器(GC)回收,导致线程对象及其相关的资源(如内存、CPU 时间片)持续占用,且无法被重新使用的情况。
简单来说,就是只生不死的线程。它与内存泄露的概念非常相似,只不过泄露的核心对象是 Thread。
每个 Java 线程都对应一个操作系统级别的线程,其开销是相当大的(默认栈大小通常为 1MB)。因此,即使只泄露少量线程,也会快速消耗掉系统的可用内存和 CPU 调度能力。
线程泄露的常见原因及示例
导致线程泄露的根本原因是:线程未能正常结束其生命周期(即未从 RUNNABLE 状态进入 TERMINATED 状态)。以下是几个典型场景:
1. 线程池使用不当(最常见的原因)
如果你使用了 ExecutorService,必须记得在不再需要时将其关闭。如果没有关闭,池中的核心线程会一直等待新的任务,从而阻止 JVM 正常退出,线程也无法被回收。
错误示例:
1 | public class ThreadPoolLeak { |
修复方法:
使用 shutdown()(优雅关闭,等待已提交任务完成)或 shutdownNow()(立即尝试停止所有任务)。
通常配合 awaitTermination 使用:
1 | executor.shutdown(); |
2. 线程被无限期阻塞或死循环
如果线程中的 run() 方法逻辑包含一个无限循环,或者线程在等待一个永远不会到来的资源(如死锁、等待一个永远不会被触发的条件、错误的 wait()/notify()、网络 I/O 阻塞没有超时等),那么该线程将永远无法结束。
错误示例(死循环):
1 | public class InfiniteThread extends Thread { |
错误示例(阻塞没有超时):
1 | public class BlockingThreadLeak { |
修复方法:
- 为阻塞操作(网络 I/O、等待锁)总是设置合理的超时时间。
- 使用中断(Interruption) 作为协作机制来取消任务。确保你的任务代码能够正确响应中断请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void run() {
while (!Thread.currentThread().isInterrupted()) { // 检查中断状态
try {
// 做一些可中断的工作,例如 Thread.sleep, BlockingQueue.take 等
Thread.sleep(1000);
} catch (InterruptedException e) {
// 收到中断信号,优雅退出循环
Thread.currentThread().interrupt(); // 重新设置中断状态
break;
}
}
System.out.println("Thread exiting gracefully.");
}
3. 持有对线程对象的强引用
即使线程已经执行完毕(进入 TERMINATED 状态),如果你在某个地方(如一个静态 Map)一直持有它的引用,那么这个 Thread 对象本身就无法被 GC 回收。虽然底层的 OS 线程已经销毁,但这仍然是一种对象泄露。
错误示例:
1 | public class ThreadReferenceLeak { |
修复方法:
- 使用
WeakHashMap等弱引用的数据结构。 - 在线程结束后,主动从集合中移除其引用(可以使用
ThreadLocal或在线程的finally块中执行清理操作)。
如何检测和诊断线程泄露?
监控工具(首选方法):
jstack: JDK 自带命令行工具。使用jstack -l <pid>可以打印出指定 Java 进程的所有线程栈信息。分析jstack的输出,查看是否有大量同名且状态相同的线程(如WAITING,RUNNABLE),这通常是泄露的迹象。- JConsole / VisualVM: JDK 自带的可视化监控工具。在线程选项卡中,你可以实时看到线程数量的变化曲线。如果线程数随着时间的推移只增不减,那基本可以确定存在线程泄露。你还可以查看每个线程的详细状态和栈跟踪。
- Java Mission Control (JMC): 更高级的商业级(对于开发/测试免费)监控工具,提供更详细的分析。
编程式监控:
- 使用
Thread.getAllStackTraces()来定期获取所有活跃线程的快照。 - 使用
ThreadPoolExecutor的 API(如getPoolSize(),getActiveCount())来监控线程池的状态。 - 集成像 Micrometer 或 Dropwizard Metrics 这样的库,将线程池指标导出到监控系统(如 Prometheus/Grafana),并设置线程数增长的告警。
- 使用
总结与最佳实践
- 优先使用线程池:避免手动创建线程,使用
ExecutorService来管理线程的生命周期。 - 务必关闭线程池:在应用程序或服务关闭时,确保正确地关闭所有线程池。
- 编写可响应的任务代码:任务代码应该能够检查中断状态并对中断请求做出响应,以便能够被取消。
- 总是设置超时:为所有可能会阻塞的操作(网络调用、锁获取等)设置合理的超时时间。
- 谨慎管理线程引用:避免在全局缓存或长期存在的对象中持有线程对象的强引用。
- 实施监控和告警:在生产环境中,对应用程序的线程数量进行持续监控,并设置阈值告警。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 技术之路!
评论


