数据库长连接未关闭不仅导致“too many connections”,更会持续占用底层网络缓冲区,引发JVM堆外内存缓慢上涨、GC不可回收,最终OOM或连接雪崩;需通过RSS内存增长、pstack线程栈、/proc/pid/maps可写区、HikariCP泄漏检测、ss/lsof连接数比对及MySQL PROCESSLIST中Sleep/Reading from net状态等多维度交叉定位与防护。

数据库长连接未关闭,不只是连接池耗尽或报“too many connections”,更隐蔽的问题是底层网络缓冲区持续占用内存,表现为JVM堆外内存(Off-heap)缓慢但稳定上涨,GC无法回收,最终触发系统级OOM或连接超时雪崩。这类泄漏不体现在堆内存监控中,容易被误判为“没泄漏”。
确认是否属于堆外网络缓冲区泄漏
先排除堆内对象泄漏(如ResultSet、Statement未关闭),聚焦网络层:
- 用 jstat -gc
观察老年代和元空间增长平缓,但应用进程 RSS 内存(ps aux --sort=-rss | head -n 10)持续上升 → 堆外嫌疑大 - 用 pstack
查看线程栈,若大量线程卡在 java.net.SocketInputStream.socketRead0或sun.nio.ch.EPollArrayWrapper.epollWait,说明连接挂起未释放,缓冲区仍在接收/等待数据 - Linux 下执行 cat /proc/
/maps | grep rw | awk '{sum += $3} END {print sum/1024/1024 " MB"}' ,观察可写内存映射区是否随运行时间线性增长
定位泄漏的 Connection 源头
不是查“谁没关close()”,而是查“谁持有了未关闭的物理连接”:
- 启用连接池的泄漏检测(以 HikariCP 为例):
spring.datasource.hikari.leak-detection-threshold=60000(单位毫秒,超60秒未归还即告警)
日志中出现 "Connection leak detection triggered" 时,会打印完整调用栈,直接定位到业务代码位置 - 抓包验证连接状态:用 ss -tulpn | grep :3306(MySQL)查看 ESTABLISHED 连接数是否远超连接池 max-size;再结合 lsof -p
| grep IPv4 | wc -l 看 Java 进程打开的 socket 数是否同步增长 - 检查是否有手动 DriverManager.getConnection() 调用 —— 绕过连接池的直连极易遗漏 close,且其 Socket 缓冲区生命周期完全由 JVM 外部 TCP 栈管理,GC 完全不可见
验证网络缓冲区是否真实堆积
从操作系统和数据库双向交叉印证:
- MySQL:执行 SHOW PROCESSLIST,关注 Time 列 > 60s 且 State 为
Sleep或Reading from net的连接;这些连接对应的内核 socket 接收缓冲区(sk_rmem_alloc)可能已积压大量未读数据 - Linux:对目标连接的 socket fd(从 /proc/
/fd/ 下找到对应 socket:xxx),用 ss -i 查看 rcv_space 和 rcv_ssthresh,若前者长期接近后者上限,说明应用层未 consume 数据,缓冲区满载滞留 - Java 层补充:若使用 Netty 或自定义 NIO,检查 Channel 是否调用了
config().setAutoRead(false)后忘记恢复,导致入站数据持续堆积在 ByteBuf 中
修复与防护措施
不能只靠“加 try-with-resources”,要切断泄漏路径:
- 禁止任何地方直接 new Socket() 或 DriverManager.getConnection();所有 DB 访问必须经由配置好的连接池 DataSource
- 连接池配置强制超时:
spring.datasource.hikari.max-lifetime=1800000(30分钟强制回收)
spring.datasource.hikari.idle-timeout=600000(空闲10分钟回收)
避免连接无限存活,即使代码漏关也能兜底 - 在 Connection 获取处埋点:用代理 DataSource 包装 getConnection(),记录调用方类名+方法+时间戳,配合定时扫描未归还连接并 dump 线程栈
- 生产环境开启 TCP keepalive:
spring.datasource.hikari.connection-init-sql=SET tcp_keepalive_time=600; SET tcp_keepalive_intvl=60;(MySQL 8.0.26+ 支持)
让内核主动探测僵死连接,加速释放底层缓冲区

















