java数据库连接未释放引发堆内与堆外双泄漏:resultset/preparedstatement钉死老年代,socket缓冲区等os资源持续占用致rss上涨、gc无效;需从应用、jvm、os、db四层交叉排查。

Java 中数据库连接未释放引发的内存泄漏,不是单纯“堆内存涨了”,而是堆内和堆外双泄漏:ResultSet/PreparedStatement 钉住老年代数据,Socket 缓冲区、文件描述符等操作系统资源持续堆积,RSS 内存上涨但 GC 完全无效。排查必须从应用层、JVM 层、OS 层、数据库层四面交叉验证。
看 RSS 内存是否异常上涨
堆内存稳定但进程实际占用内存(RSS)持续增长,是堆外泄漏的强信号:
- 执行 ps aux --sort=-rss | head -n 5,观察 Java 进程 RSS 是否随运行时间线性上升
- 配合 jstat -gc
,确认老年代、元空间、Eden 区变化平缓 —— 若堆内没明显增长,问题大概率在堆外 - 用 cat /proc/
/maps | grep rw | awk '{sum += $3} END {print sum/1024/1024 " MB"}' 统计可写内存映射总量,Socket、DirectBuffer、eventpoll 等都会计入其中
启用并验证连接池泄漏检测
HikariCP 的 leak-detection-threshold 不是配了就生效,必须确保日志能打出调用栈:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 配置 spring.datasource.hikari.leak-detection-threshold=30000(建议设为业务最长耗时的 1.5 倍)
- 必须同时开启 spring.datasource.hikari.log-connection-warnings=true,否则警告不输出
- 触发后日志会出现 "Connection leak detection triggered",附带完整调用栈,精准定位到哪一行 getConnection() 拿了没还
- 注意:只 close Connection 不够,try-with-resources 必须包含 ResultSet 和 PreparedStatement
查操作系统和数据库连接状态
连接是否真“挂住”,不能只看代码逻辑,要从系统和 DB 双向印证:
- 执行 ss -tulpn | grep :3306,比对 ESTABLISHED 连接数与 HikariCP 的 maxPoolSize —— 若远超配置值,说明连接未归还
- 执行 lsof -p
| grep IPv4 | wc -l ,确认 Java 进程打开的 socket 数同步增长 - 登录 MySQL,运行 SHOW PROCESSLIST,重点关注 State = Sleep 或 Reading from net、Time > 60 的连接 —— 这些就是卡在 TCP 接收缓冲区、持续占内存的“僵尸连接”
抓堆快照定位堆内钉死对象
如果 RSS 上涨的同时堆内存也在涨,说明 ResultSet/PreparedStatement 也漏关了:
- 用 jmap -dump:format=b,file=heap.hprof
抓取堆快照 - 用 MAT 打开,点 Leak Suspects 报告,重点关注 com.mysql.cj.jdbc.result.ResultSetImpl 和 com.mysql.cj.jdbc.ClientPreparedStatement
- 检查引用链:是否由未关闭的 PreparedStatement 强引用 ResultSet,导致整批查询结果(含字段元数据、原始字节)常驻老年代
- 特别注意 MySQL 8.0+ 驱动默认开启元数据缓存,ResultSet 不 close,缓存不会清
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










