You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何排查MySQL驱动代码死锁及压测引发的OOM关联死锁问题

我来帮你一步步拆解这个问题——从确认死锁根源到定位MySQL驱动里的具体问题,再解决OOM的诱因。

第一步:先把死锁的锁持有关系摸清楚

既然线程dump已经发现死锁,首先得把谁拿着什么锁、谁在等什么锁理清楚。你提到了http-nio-8080-exec-9业务线程和Finalizer线程,从thread dump里提取这两个线程的关键信息:

  • http-nio-8080-exec-9当前持有哪些锁?它在等哪个锁?
  • Finalizer线程(名字就是Finalizer)手里攥着哪些锁?它又在等哪个锁?

举个典型的死锁场景,jstack输出里会明确标出来:

Found one Java-level deadlock:
=============================
"Finalizer":
  waiting to lock monitor 0x00007f9e18006868 (object 0x000000076ab4d0a0, a com.mysql.cj.jdbc.result.ResultSetImpl),
  which is held by "http-nio-8080-exec-9"
"http-nio-8080-exec-9":
  waiting to lock monitor 0x00007f9e18004c18 (object 0x000000076ab4d100, a com.mysql.cj.jdbc.ConnectionImpl),
  which is held by "Finalizer"

如果是这种循环等待,那就是板上钉钉的死锁了。

第二步:定位Finalizer线程死锁的触发点

你提到Finalizer线程卡在ResultSetImpl.realClose()——这基本说明你的代码里没有显式关闭ResultSet/Statement/Connection,导致这些对象被GC标记后,扔给Finalizer线程来擦屁股。而realClose()里肯定要拿锁(比如ResultSet自身的锁,或者关联Connection的锁),如果此时业务线程还攥着这个锁不放,Finalizer就会卡在这里。

接下来要盯紧http-nio-8080-exec-9执行的那行代码:

  • 那行代码在干嘛?是在遍历ResultSet读取数据,还是拿着Connection的时候去做其他阻塞操作了?
  • 有没有可能业务线程拿着ResultSet的锁,同时在等Connection的资源;而Finalizer线程回收ResultSet时,先抢ResultSet的锁(抢不到,被业务线程拿着),然后又去操作Connection(比如归还到连接池),把Connection的锁攥在了手里,导致业务线程也拿不到——这不就形成死锁了嘛!

举个常见的错误操作:业务线程没关ResultSet,拿着它的引用去调用一个慢得要死的外部HTTP接口,这时候GC触发,Finalizer线程想回收这个ResultSet,调用realClose()要拿ResultSet的锁,但业务线程还攥着;等业务线程HTTP调用完,想操作Connection,发现Connection的锁被Finalizer拿着——死锁就这么产生了。

第三步:结合MySQL驱动代码捋清楚死锁逻辑

咱们直接看ResultSetImpl.realClose()的源码(以Connector/J 8.x为例),它的逻辑大概是这样:

  1. 先同步当前ResultSet对象(synchronized (this)),防止并发操作;
  2. 清理关联的Statement资源,或者调用Statement的close方法;
  3. 如果关联的Connection还存活,会和Connection交互(比如把Statement从Connection里移除,或者归还资源到连接池);
  4. 释放ResultSet持有的所有内存、游标资源。

如果你的业务线程(exec-9)此时正攥着ResultSet的锁(比如遍历ResultSet时卡住,或者拿着锁去做其他阻塞操作),Finalizer线程进入realClose()就会卡在synchronized (this)这里,等业务线程释放锁。要是这时候业务线程又在等Finalizer手里的Connection锁,那死锁就没跑了。

第四步:验证你的推测,然后解决问题

验证Finalizer死锁导致OOM的猜想

你可以通过这几个方式实锤:

  • 看GC日志,是不是有大量Finalizer对象排队等着被回收(比如Finalizer队列长度一直涨);
  • 拉内存快照(heap dump),看看是不是堆里堆了一大堆没被回收的ResultSetImpl/StatementImpl/ConnectionImpl,它们的引用链都指向Finalizer队列;
  • 观察死锁发生时,Finalizer线程是不是一直处于WAITING状态,业务线程也卡在WAITING不动。

解决方法,按优先级来

  1. 强制显式关闭资源(最核心):业务代码里必须用try-with-resources语法(Java 7+),显式关闭ResultSet、Statement、Connection,不让Finalizer线程插手:
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT ...")) {
    // 处理ResultSet的逻辑
} catch (SQLException e) {
    // 异常处理
}

try块结束后,资源会自动关闭,根本不会进入Finalizer队列,从根源上避免Finalizer参与回收操作。

  1. 排查业务线程的阻塞点:盯着http-nio-8080-exec-9执行的那行代码,看看是不是拿着数据库资源的时候去做了耗时阻塞操作(比如调用外部服务、等锁、慢IO)。如果是,把数据库操作和阻塞操作分开:先读完ResultSet,关闭所有资源,再去做耗时操作。

  2. 升级MySQL驱动版本:有些旧版本的Connector/J存在Finalizer相关的死锁bug,比如锁顺序不合理之类的,升级到最新稳定版(比如8.0.33+)说不定能直接解决已知问题。

  3. 临时workaround(不推荐长期用):如果暂时改不了代码,可以试试调整JVM参数:-XX:+DisableExplicitGC(避免显式GC触发Finalizer),或者-XX:FinalizerTimeout=3000(给Finalizer线程设个超时时间),但这只是治标不治本。

总结

你的推测大概率是对的:Finalizer线程因为死锁没法完成资源回收,导致大量未关闭的数据库对象堆在内存里,最后撑爆OOM。核心解决思路就是不让Finalizer线程碰数据库资源(显式关闭),同时修正业务线程里拿着数据库资源瞎阻塞的操作,理顺锁的获取顺序。

内容的提问来源于stack exchange,提问作者Daniel Kaplan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 04:18:26