提要
但是,在普遍的使用不当的情形中,最多的问题便是没有及时
释放连接
,这里的释放是指将
Connection
对象归还连接池。若连接未被释放,则连接池将被很快耗尽(Exhausted),从而无法提供新的连接,最终导致应用不能进行数据库操作,并在尝试获取新的连接时出现以下异常:
1 |
... |
有关JVM内存转储方式的说明见 JVM内存分析:Tomcat内存泄漏 。
在本案例应用(约定称为「应用A」)的使用过程中偶尔会在前端弹出
Timeout waiting for idle object
的异常提示框。经过查看完整的异常堆栈(看上面)可发现,异常发生在从连接池获取
Connection
时,在对
GenericObjectPool
源码分析后可初步确定是因为连接池已满而无法分配新的
Connection
造成的。
为进一步确认该问题,将应用A的内存转储(
sudo -u tomcat jmap -dump:format=b,file=heap-dump.bin <java_pid>
)并通过
Eclipse Memory Analyzer - MAT
对其内存进行分析。
点击工具栏中的
OQL
图标,这里需要通过
OQL
进行一些复杂的过滤查询(OQL:
SELECT OBJECTS ds FROM org.apache.commons.dbcp.BasicDataSource ds
):
Help -> Help Contents
菜单中进入帮助手册查询到;
org.apache.commons.dbcp.BasicDataSource
为应用中使用的
DataSource
的实现类,其内部引用
org.apache.commons.dbcp.PoolingDataSource
,并在
PoolingDataSource
中负责从连接池申请新的连接;
从图中可以看到,连接池
org.apache.commons.pool.impl.GenericObjectPool
的
_numActive
为
749
,而在应用中为其分配的最大活跃连接数(
maxActive
)为
750
。因此,可以进一步确定连接池的确已达到分配上限,在并发情况下将不会再分配更多连接,从而导致等待超时并抛出异常。
再看看内存中是否存在未被释放的MySQL连接。
由于MAT默认不分析
unreachable
对象,所以,在开始前需通过其自带的工具
ParseHeapDump.sh
(或
ParseHeapDump.bat
)
分析不可达对象
:
1 |
# 运行该命令前需删除dump文件所在目录中由MAT生成的分析文件 |
点击MAT的菜单
File -> Open Heap Dump
选择文件
heap-dump.bin
载入转储分析文件,然后,通过OQL(
SELECT cn, cn.isClosed, cn.io.mysqlConnection, cn.io.mysqlConnection.closed FROM INSTANCEOF com.mysql.jdbc.JDBC4Connection cn
)得到MySQL Connection对象如下:
点击工具栏中的分组图标可按照Class Loader对结果进行分组;
在MySQL驱动中的
Connection
所引用的相关对象为:
可以发现,在36个连接中仅有2个是正常关闭的,其余的
Connection
未被关闭,但对应的
Socket
连接却处于关闭状态。这里可以假设出以下两种可能的情形:
Connection
在
使用时
出现网络中断,导致
Socket
非正常关闭;
Connection
在
使用后
未被正常关闭,并且在某个时刻发生了
Socket
连接中断;
对于第一种情形,
Socket
的异常关闭势必会抛出异常,并最终在使用方拦截到该异常并关闭
Connection
,而这里的
Connection
为非关闭状态,说明使用方并未准确做资源的释放处理。第二种情形,自然也是因为资源未被及时释放了。
因此,
Connection
没有被及时、准确地释放是相当肯定的事情了。
但这里依然有个疑问,为啥连接池里记录分配的连接为
749
,而实际查到的
Connection
对象只有30多个,其余的哪儿去了?
针对上述问题,限于基本功的问题,目前还没有确切的定论,但大致可推断是MySQL驱动中的
com.mysql.jdbc.AbandonedConnectionCleanupThread
对弱引用的
com.mysql.jdbc.JDBC4Connection
对象做了资源
主动回收
处理:
com.mysql.jdbc.NonRegisteringDriver.ConnectionPhantomReference
为虚引用类
java.lang.ref.PhantomReference
的扩展类,其将在
java.lang.ref.Reference.ReferenceHandler#run
中等待JVM启动GC时进行清理活动。
Java引用相关的知识可阅读 详解java.lang.ref包中的4种引用 。
既然得到了看似很有道理的分析结论,那就应该有方法复现当前的问题。
首先,通过IDE工具查找应用中主动获取
Connection
对象而未释放的代码位置,最终找出以下几处:
经过调用分析后,可找到以下几个相关的Web API以用于复现验证:
需要注意的是,一般MySQL服务端会限定并发连接数,为了快速复现当前问题,可通过以下语句调整MySQL的默认最大连接数(重启后将失效):
1 |
-- 查看默认配置: show variables like '%connect%'; |
在登录应用A后,在浏览器控制台中执行以下代码便可复现最开始提到的异常问题:
1 |
var ctx = '/a'; |
解决方案
try {...} finally {...}
语句的
finally
块中关闭
主动获取
的
Connection
对象。
而本案例中的业务需求似乎比较特殊,具体方案需根据实际业务需求确定,但必须坚持以下原则:
finally
块中释放主动获取的
Connection
对象;