这不是死锁而是连接池耗尽与内存泄漏叠加导致的假死:jstack无“found 1 deadlock”,线程阻塞在getconnection()或tostring(),druid监控显示activecount=maxactive且waitthreadcount持续上升。

这其实不是连接池“死锁”,而是连接池耗尽 + 内存泄漏叠加引发的假死现象。把异常堆栈转成超长字符串并持久化,会同时冲击两个关键资源:JVM堆内存和数据库连接。问题表象常被误判为“死锁”,但jstack里看不到线程互相等待锁,只看到大量线程阻塞在getConnection()或toString()上。
确认是否真为连接池耗尽而非死锁
先排除经典死锁干扰:
- 执行
jstack -l <pid></pid>,若输出中没有Found 1 deadlock字样,基本可排除线程级死锁 - 检查线程状态:如果大量线程停留在
com.alibaba.druid.pool.DruidDataSource.getConnection()或java.lang.Throwable.getStackTraceAsString(),说明是阻塞或卡顿,不是死锁 - 对比
Druid监控页面(如/druid/index.html)中的ActiveCount(活跃连接数)是否长期等于MaxActive(最大连接数),且WaitThreadCount持续上升——这是连接池耗尽的铁证
定位超长字符串生成源头
异常堆栈转字符串本身不危险,危险在于它被不当复用、缓存或拼接后写入数据库:
- 搜索代码中类似
e.toString()、ExceptionUtils.getFullStackTrace(e)、Arrays.toString(e.getStackTrace())等调用,尤其关注日志记录、告警上报、数据库插入等场景 - 重点检查是否在循环中反复调用(例如每条失败消息都生成完整堆栈)、是否与业务字段拼接(如
"订单失败:" + orderId + ",错误:" + e.toString()),这会导致字符串对象爆炸式增长 - 查看是否有自定义的
ExceptionLogService.save(e)类方法,其内部是否未做截断、未异步处理、未限制长度
验证内存与连接的双重挤压
超长字符串会直接拖垮两个环节:
-
内存侧:一个深度为100层的异常堆栈,转成字符串后轻松超过500KB;高频报错时,大量
char[]堆积在老年代,触发频繁GC甚至OOM;Druid 1.1.22等旧版本还会将该SQL(含长字符串)缓存进sqlStatMap,进一步锁死内存 - 连接侧:持久化这个超长字符串时,JDBC驱动需分配大缓冲区、网络传输耗时增加、数据库写入慢(尤其MySQL对长TEXT字段加锁时间更长),导致单次DB操作从几毫秒拉长到数秒,连接被长时间占用,新请求排队等待
- 两者叠加:GC停顿期间线程暂停,连接无法释放;连接耗尽后更多线程卡在获取连接处,加剧CPU空转和线程堆积
快速缓解与修复建议
无需重启即可临时止损,并推动根治:
- 立即限制日志/落库内容长度:对
getStackTraceAsString()结果做substring(0, 2048)截断,或改用e.getMessage() + " | " + e.getClass().getSimpleName()代替全堆栈 - 禁止在事务内持久化原始异常堆栈:改为先记录简要信息,再通过异步线程或消息队列将完整堆栈写入独立日志表或ELK系统
- 升级Druid至1.2.16+,关闭SQL统计功能:
druid.stat.mergeSql=false、druid.stat.slowSqlMillis=0,避免缓存污染 - 为关键落库操作单独配置短超时连接池(如
maxWait=500ms),避免一个慢写拖垮全局连接










