WebappClassLoader 与 SPI 混合使用引发驱动死锁,本质是类加载阻塞与服务发现初始化竞争叠加所致,表现为线程卡在 Class.forName、ServiceLoader.load 或驱动静态块,堆栈中 WebappClassLoader 与 AppClassLoader 交替调用并出现重复锁地址。

排查 WebappClassLoader 与 SPI 混合使用引发的驱动死锁,核心在于识别“类加载阶段阻塞”与“服务发现初始化竞争”的叠加效应。这类问题不表现为标准 JDBC 连接超时,而是线程卡在 Class.forName、ServiceLoader.load 或驱动静态块内,且堆栈中频繁出现 WebappClassLoader 和 sun.misc.Launcher$AppClassLoader 交替调用痕迹。
看线程栈,锁定阻塞源头
用 jstack <pid> 抓取全量线程快照后,重点搜索以下特征:
- 状态为
WAITING或BLOCKED,且调用链含WebappClassLoader.loadClass、Class.forName、ServiceLoader.load、DriverManager.getDrivers - 堆栈中出现重复的锁地址(如
waiting to lock <0x...>与locked <0x...>成对出现) - 多个线程在加载同一驱动类(如
com.mysql.cj.jdbc.Driver)或 protobuf/GRPC 相关类时互相等待 - 存在
java.lang.ClassLoader.findLoadedClass调用后长时间无返回——说明类尚未定义完成,但其他线程已尝试获取其锁
查类加载器归属与双亲委派断裂
SPI 机制默认使用线程上下文类加载器(Thread.currentThread().getContextClassLoader()),而 Web 应用中该加载器通常是 WebappClassLoader。若驱动 jar 同时被多个 classloader 加载(如:系统类加载器加载了部分依赖,WebappClassLoader 又尝试加载驱动本身),就会触发隐式锁竞争。
- 确认驱动 JAR 是否同时存在于
$JAVA_HOME/jre/lib/ext和应用WEB-INF/lib——这是典型双加载诱因 - 检查是否显式设置了
Thread.currentThread().setContextClassLoader(...),尤其在异步线程(如CompletableFuture、定时任务)中误设为系统类加载器 - 验证自定义类加载器是否重写了
loadClass方法,绕过双亲委派;正确做法仅重写findClass,并在其中调用super.findLoadedClass(name)避免重复 define
验驱动初始化路径与静态块依赖
MySQL 8+、PostgreSQL 等新版驱动将 Driver 注册逻辑从 static {} 移至 java.sql.Driver 的 ServiceLoader 发现机制,但部分封装层(如 Druid、HikariCP)仍会主动调用 Class.forName(driverClassName) 触发初始化。若该类内部又依赖未加载的 SPI 接口实现(如日志适配器、SSL 工厂),就可能形成加载闭环。
- 用
javap -v com.mysql.cj.jdbc.Driver查看其<clinit>内容,确认是否含外部类引用 - 检查日志框架(Log4j2、SLF4J)绑定是否通过
WEB-INF/lib提供,还是由容器共享——混合绑定易导致org.slf4j.LoggerFactory初始化卡住 - 禁用自动注册:在 JDBC URL 中添加
?allowPublicKeyRetrieval=true&useSSL=false等参数降低初始化复杂度;或设置系统属性jdbc.drivers预加载驱动,避开运行时动态发现
复现与隔离验证
该类死锁具有强时序敏感性,常在应用启动后首次并发请求时爆发,重启后暂时消失。建议构造最小复现场景:
- 写一个 Servlet,在
doGet中并发执行 5–10 次Class.forName("com.mysql.cj.jdbc.Driver")+ServiceLoader.load(Driver.class) - 关闭连接池(如 Druid 的
init设为 false),排除连接获取干扰,专注类加载路径 - 使用
-XX:+TraceClassLoading和-XX:+TraceClassUnloading输出类加载日志,观察驱动类及其依赖类的加载顺序和线程归属



















