数据库连接未关闭引发的内存泄漏本质是Socket缓冲区、Netty DirectBuffer等堆外资源驻留,JVM GC不可见,表现为RSS内存线性上涨而堆内存平稳,需通过jstat、ps、/proc/pid/maps、ss、SHOW PROCESSLIST及HikariCP泄漏检测多维定位。

数据库连接未正常关闭引发的内存泄漏,本质不是堆内对象堆积,而是 Socket 缓冲区、Netty DirectBuffer、MySQL 驱动内部接收队列等堆外资源持续驻留。JVM 垃圾回收对此完全不可见,所以堆内存监控往往“风平浪静”,而进程 RSS 内存却稳步上涨,最终触发系统级 OOM 或连接雪崩。
看内存特征:先区分是堆内还是堆外泄漏
别一上来就翻代码。先用基础命令快速定性:
- jstat -gc <pid> 1000 5:观察老年代(OU)、元空间(MU)是否缓慢增长;若几乎不动,但服务越来越卡 → 堆外嫌疑大
- ps aux --sort=-rss | head -n 10:查 RSS 内存是否随运行时间线性上升(比如每小时涨 50–100MB)
- cat /proc/<pid>/maps | grep rw | awk '{sum += $3} END {print sum/1024/1024 " MB"}':统计可写内存映射区大小,持续增长即指向堆外缓冲区堆积
查连接状态:从 OS 和数据库双向印证
单看 Java 层容易漏掉绕过连接池的直连或已挂起的“僵尸连接”:
-
ss -tulpn | grep :3306:查看 ESTABLISHED 连接数。若远超 HikariCP 的
maximum-pool-size(比如池设 20,实际看到 80+),说明有连接没归还 - lsof -p <pid> | grep IPv4 | wc -l:确认 Java 进程打开的 socket 数是否同步飙升
-
MySQL 中执行 SHOW PROCESSLIST:重点筛选
State为Sleep或Reading from net、且Time> 60 秒的连接——这些就是持有着内核 sk_rmem_alloc 缓冲区的“活体泄漏源”
启用并信任连接池的泄漏检测
HikariCP 的 leak-detection-threshold 不是摆设,关键在配得准、日志看得见:
立即学习“Java免费学习笔记(深入)”;
- 设为业务逻辑预期最大耗时的 1.5 倍,例如查询通常 2 秒完成,建议设
30000(毫秒),而非拍脑袋的 60000 - 必须配合
spring.datasource.hikari.log-connection-statement=true,才能在日志中看到每次 getConnection() 和 connection.close() 的时间戳与堆栈 - 一旦出现
Connection leak detection triggered日志,它会打印完整调用链,精准定位到哪一行getConnection()后没归还,甚至能看见是哪个 Service 方法里漏掉了 try-with-resources
盯紧 ResultSet 和 Statement 的释放逻辑
很多人以为关了 Connection 就万事大吉,其实 MySQL 8.0+ 驱动默认缓存元数据和部分结果行,ResultSetImpl 对象不 close,整块结果集就钉在堆里:
- 用
jmap -dump:format=b,file=heap.hprof <pid>抓堆转储,MAT 中按 “Leak Suspects” 查看是否大量com.mysql.cj.jdbc.result.ResultSetImpl占据老年代 - 检查所有数据库操作是否严格使用
try (Connection c = ...; PreparedStatement ps = ...; ResultSet rs = ...) { ... }—— 三个资源缺一不可 - 禁用
DriverManager.getConnection()手动建连,这种直连既不走连接池回收机制,也不受泄漏检测约束,Socket 生命周期完全脱离 JVM 管控


















