本质是底层socket缓冲区等堆外资源持续驻留,需分层排查:先依rss与堆内存增长特征区分堆内/堆外泄漏,再通过hikaricp泄漏检测、show processlist、ss/lsof等锁定未关闭connection源头,最后验证resultset/statement等三级资源释放逻辑。

Java中连接池未关闭引发的内存泄漏,本质不是“池子满了”,而是底层资源(Socket缓冲区、ResultSet数据、Netty DirectBuffer等)持续驻留,堆内堆外同步失控。排查要分层聚焦:先确认泄漏类型,再锁定资源源头,最后验证释放逻辑。
看内存增长特征,区分堆内还是堆外泄漏
堆内泄漏表现明显:老年代持续上涨、Full GC后不回落、堆转储里大量 JDBC/Redis 对象(如 ResultSetImpl、Jedis、RedissonConnection)。用 jmap -dump + MAT 查 “Leak Suspects” 和 GC Roots 引用链即可定位。
堆外泄漏更隐蔽:RSS 内存(ps aux --sort=-rss)持续上升,但堆内存平稳;jstat -gc 显示元空间和老年代无压力;/proc/pid/maps | grep rw 可写映射区线性增长。此时重点查网络层——MySQL 的 SHOW PROCESSLIST 中大量 Sleep 或 Reading from net 状态,或 Linux 的 ss -tulpn 显示 ESTABLISHED 连接数远超连接池配置上限。
逐个检查 JDBC 连接池泄漏点
JDBC 泄漏常发生在 ResultSet、Statement、Connection 三级资源未闭环:
- 只关了 PreparedStatement 却漏掉 ResultSet:MySQL 8.0+ 驱动默认缓存结果集元数据和部分行数据,
rs.close()不调,堆内存就一直扛着 - try-with-resources 漏声明:写了
Connection和PreparedStatement,却没把ResultSet加进去,导致 rs 生命周期失控 - 手动 DriverManager.getConnection():绕过连接池直连,close 完全靠开发者自觉,Socket 缓冲区 GC 完全不可见
- 预编译缓存开启后复用 PreparedStatement:若之前生成的 ResultSet 没清理干净,缓存对象内部仍强引用旧结果集,越积越多
启用 HikariCP 泄漏检测:spring.datasource.hikari.leak-detection-threshold=60000,日志出现 Connection leak detection triggered 时,会打印完整调用栈,直接定位到哪一行代码借了没还。
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
逐个检查 Redis 连接池泄漏点
Redis 泄漏高频在连接未归还、监听器未注销、迭代器未关闭三类:
- JedisPool 资源未 close:老版本 Jedis 必须显式
jedis.close(),不能只依赖 try-with-resources(需确认 Jedis 版本是否实现 AutoCloseable) - Redisson 的 RTopic 监听器注册后未 remove:
topic.addListener()返回 listenerId,业务结束必须配对调用topic.removeListener(id) - RMapCache 或 RBucket 未设 TTL:缓存对象长期存活,本地缓存 + Redis 数据双份堆积;尤其
RMap.listIterator()返回的迭代器必须手动iterator.close() - Redisson Netty 线程泄漏:
Thread.getAllStackTraces()查 redisson-netty- 前缀线程是否持续增长,watchdogTimeout 或 idleTimeout 配置不合理会导致续期线程卡住
用 redisson.getNodesGroup().pingAll() 快速验证集群连通性;通过 manager.getConnectionPool().getActiveConnections() 实时读取活跃连接数,比监控指标更准。
统一加固释放逻辑,不依赖“自动”
别信“连接池会兜底”或“框架自动管理”——HikariCP 管连接,不管 ResultSet;Spring RedisTemplate 封装了 getConnect(),但若底层用的是 JedisPool 且你又手动取了 Jedis,那释放责任仍在你手上。
所有资源必须显式 close,优先用 try-with-resources 一次性声明全部:
- JDBC:
try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(...); ResultSet rs = ps.executeQuery()) { ... } - Redisson:
try (RLock lock = redisson.getLock("key")) { lock.lock(); ... } - Jedis:
try (Jedis jedis = jedisPool.getResource()) { ... }(确认 Jedis ≥ 3.0)
异常分支也要覆盖:catch 块里吞异常、finally 里忘记判空,都会让 close 失效。最稳妥写法是 finally 中判空 + close,并捕获 close 异常不抛出。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










