bigywater.tech
Java · 378

测试并发资源泄漏

资源泄漏通常只在并发或异常路径下出现,普通的单元测试看不到。这篇用文件描述符、线程和连接池三种资源实测:什么样的测试能发现泄漏,什么样的测试会漏掉它或误报,以及 System.gc() 如何影响结论。

+285
16 线程并发 2000 次:异常路径没关流造成的 fd 增量(3 次调用的测试是 +0)
+400
每次请求新建线程池且不 shutdown,200 次调用后的线程数增量
0/10
连接池泄漏后并发 200 次结束时的空闲连接(修复版 10/10)

测不到的泄漏

实测 · 本机执行输出
  calls=3 单线程 成功3 失败0 fd基线6 之后6 增量0 | System.gc()后6 增量0
  calls=300 单线程 成功258 失败42 fd基线6 之后48 增量42 | System.gc()后6 增量0
  calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后6 增量0

GC 洗掉泄漏

实测 · 本机执行输出
  默认 GC:
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后6 增量0
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后6 增量0
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后6 增量0
  -XX:+UseEpsilonGC:
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后291 增量285
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后291 增量285
    calls=2000 16线程 成功1715 失败285 fd基线6 之后291 增量285 | System.gc()后291 增量285
  fixed calls=2000 成功1715 失败285 fd基线6 之后6 增量0

线程泄漏

实测 · 本机执行输出
  leaky n=1 线程基线6 之后8 增量2 | System.gc()后8 增量2
  fixed n=1 线程基线8 之后8 增量0
  leaky n=10 线程基线6 之后26 增量20 | System.gc()后26 增量20
  fixed n=10 线程基线26 之后26 增量0
  leaky n=50 线程基线6 之后106 增量100 | System.gc()后106 增量100
  fixed n=50 线程基线106 之后106 增量0
  leaky n=200 线程基线6 之后406 增量400 | System.gc()后406 增量400
  fixed n=200 线程基线406 之后406 增量0

还没结束与泄漏

实测 · 本机执行输出
  shutdown() 后不等待:每 50 次调用后立即测到的线程增量 [100, 200, 300, 400];最后一次调用后立即 400;再等到增量归零用了约 30 ms,归零后增量 0
  shutdown() 后不等待:每 50 次调用后立即测到的线程增量 [100, 200, 300, 400];最后一次调用后立即 400;再等到增量归零用了约 30 ms,归零后增量 0
  shutdown() 后不等待:每 50 次调用后立即测到的线程增量 [100, 200, 300, 400];最后一次调用后立即 400;再等到增量归零用了约 25 ms,归零后增量 0

连接池泄漏

实测 · 本机执行输出
  泄漏版 200 次并发调用:成功64 失败136(其中拿不到连接而超时 126)空闲连接 0/10
  之后再调用 5 次(都是不会失败的 i=1):成功0 超时5
  修复版 200 次并发调用:成功172 失败28(其中拿不到连接而超时 5)空闲连接 10/10

分步演示

资源泄漏测试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;fd 用 com.sun.management.UnixOperatingSystemMXBean,线程数用 ThreadMXBean;被测代码与泄漏检测函数都是自写的(没有使用 JUnit 的 leak 插件或 Netty ResourceLeakDetector)
    • 补充观察:第一版实验里,调用方先 get() 再 shutdown(),没有等待也量不到残留线程(3 次运行都是 0),所以第 4 步改成了调用方不等待的场景;没有验证:Linux 上 /proc/self/fd 的计数方式、Socket / 文件通道 / DirectByteBuffer 的泄漏、虚拟线程、Netty 的采样式泄漏检测、真实数据库连接池(HikariCP 的 leakDetectionThreshold)
    京ICP备2026067502号-1京公网安备11010502063487号