1. 问题背景
线上出现内存报警,内存增长曲线如下
dump内存文件,临时重新发布服务。后经排查发现是数据库连接池设置不合理以及mysql-connector-java 5.1.49有内存泄漏bug。以下为对此问题的分析及问题总结。
1.1 应用背景
数据库连接池: HikariCP
mysql-connector-java : 5.1.49版本
HikariCP配置:
2.问题分析
2.1 dump分析
通过 MAT 工具分析发现,com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference 实例较多且无法回收 如下图:
然后看大对象列表,NonRegisteringDriver 对象确实占内存比较多,其中成员变量connectionPhantomRefs
占内存最多,里面存的是数据库连接的虚引用,其类型是 ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>
,占比最多
2.2 源码分析
mysql创建连接源码:
com.mysql.jdbc.NonRegisteringDriver#connect
public Connection connect(String url, Properties info) throws SQLException {
if (url != null) {
if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://")) {
return this.connectLoadBalanced(url, info);
}
if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://")) {
return this.connectReplicationConnection(url, info);
}
}
Properties props = null;
if ((props = this.parseURL(url, info)) == null) {
return null;
} else if (!"1".equals(props.getProperty("NUM_HOSTS"))) {
return this.connectFailover(url, info);
} else {
try {
// 这里创建连接
com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(this.host(props), this.port(props), props, this.database(props), url);
return newConn;
} catch (SQLException var6) {
throw var6;
} catch (Exception var7) {
SQLException sqlEx = SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + var7.toString() + Messages.getString("NonRegisteringDriver.18"), "08001", (ExceptionInterceptor)null);
sqlEx.initCause(var7);
throw sqlEx;
}
}
}
而 ConnectionImpl类在初始化构造的时候会调用NonRegisteringDriver.trackConnection(this);方法,而这个方法我们看命名就知道是追踪数据库连接的,我们接着往下看
public class NonRegisteringDriver implements Driver {
protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap();
protected static final ReferenceQueue<ConnectionImpl> refQueue = new ReferenceQueue();
protected static void trackConnection(com.mysql.jdbc.Connection newConn) {
// 就是这里声明虚引用ConnectionPhantomReference,放入ReferenceQueue
ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl)newConn, refQueue);
connectionPhantomRefs.put(phantomRef, phantomRef);
}
}
第一行代码声明了一个名为connectionPhantomRefs的ConcurrentHashMap容器,该容器用于存储ConnectionPhantomReference实例。
第二个方法trackConnection的作用是将新连接添加到connectionPhantomRefs映射中。它接受一个com.mysql.jdbc.Connection对象作为参数,创建一个新的ConnectionPhantomReference实例,并使用它和引用队列(refQueue)将其添加到connectionPhantomRefs映射中。
总的来说,这两个代码片段旨在通过使用虚引用来实现跟踪连接到MySQL数据库的机制。虚引用用于跟踪已被JVM垃圾回收的对象,允许程序在对象从内存中删除后执行特定任务。
static class ConnectionPhantomReference extends PhantomReference<ConnectionImpl> {
private NetworkResources io;
ConnectionPhantomReference(ConnectionImpl connectionImpl, ReferenceQueue<ConnectionImpl> q) {
super(connectionImpl, q);
try {
this.io = connectionImpl.getIO().getNetworkResources();
} catch (SQLException var4) {
}
}
void cleanup() {
if (this.io != null) {
try {
this.io.forceClose();
} finally {
this.io = null;
}
}
}
}
ConnectionPhantomReference后置处理如上,leanup() 方法用于在连接对象被垃圾回收后清理网络资源。它检查 io 属性是否为空,如果不为空,则调用 forceClose() 方法来强制关闭底层网络资源,最终将 io 属性设置为 null。整个过程确保连接对象被垃圾回收时,底层网络资源也被正确地释放。
MySQL为什么要使用虚引用来解决IO资源回收问题?
MySQL 使用虚引用来解决 IO 资源回收问题,主要是因为 JDBC 连接对象在关闭连接时无法保证其底层网络资源会被立即释放。这可能会导致底层网络资源长时间占用,最终导致应用程序出现性能下降或者资源耗尽的情况。
使用虚引用的好处在于,它允许程序在对象从内存中删除后执行特定任务。 MySQL 驱动程序利用 Java 提供的引用队列机制,将 JDBC 连接对象的虚引用加入到队列中。一旦连接对象被垃圾回收,JVM 会将它加入到引用队列中等待进一步处理。此时,MySQL 驱动程序通过监视引用队列并清理底层网络资源,确保这些资源在连接对象被垃圾回收时被正确地释放,从而避免了底层网络资源长时间占用的问题。
以下是虚引用的主要特点:
1.不影响对象的生命周期: 虚引用的存在并不会延长对象的生命周期。即使对象被虚引用引用着,只要没有其他强引用、软引用或者弱引用指向该对象,它也会被垃圾回收器回收。
2.用于跟踪对象的回收状态: 虚引用主要用于跟踪对象被回收的状态。当对象被垃圾回收器回收时,虚引用会被添加到与之关联的引用队列中,可以通过检查引用队列来得知对象已经被回收。
3.无法通过虚引用获取对象: 虚引用不可以直接通过 get() 方法获取到对象的引用,它的 get() 方法始终返回 null。因此,虚引用主要用于进行对象回收状态的跟踪,而无法用于获取对象的引用。
那MySQL是怎样执行最终的IO资源回收的呢,是使用了定时线程还是异步守护线程?
是使用异步守护线程处理的
所以NonRegisteringDriver的静态成员变量:connectionPhantomRefs, 有几万个对象, 说明了在这段时间积累了大量的数据库连接connection实例进入以下生命周期:
创建 --> 闲置 —> 回收;
我们再回头看看我们的数据库连接池的配置是怎么样的
maxPoolSize: 80 同事写的,根本就没有这个属性-_- 所以最大默认连接数是10 maximumPoolSize控制最大连接数,默认为10
minIdle: 2 这个属性也写错了-_- minimumIdle控制最小连接数,默认等同于maximumPoolSize,10。
idleTimeout: 600000 连接空闲时间超过idleTimeout 10min后连接被抛弃 此设置仅适用于minimumIdle 定义为小于maximumPoolSize 的情况
maxLifetime: 1800000 连接生存时间超过 maxLifetime 30分钟后,连接会被抛弃.
根据上述配置可知,每隔 30 min,就会重新创建一批连接实例放入内存中,而每次新建一个数据库连接,都会把该连接放入connectionPhantomRefs集合中。
因为连接资源一般存活时间比较久,经过多次Young GC,一般都能存活到老年代。如果这个数据库连接对象本身在老年代,connectionPhantomRefs中的元素就会一直堆积,直到下次 full gc。同时如果等到full gc 的时候connectionPhantomRefs集合的元素非常多,full gc也会非常耗时。
问题找到了,哪解决方案就呼之欲出了,见下面
3.解决方案
3.1 调整HikariCP配置
HikariCP默认配置(参考:https://github.com/brettwooldridge/HikariCP)
maximumPoolSize
This property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. A reasonable value for this is best determined by your execution environment. When the pool reaches this size, and no idle connections are available, calls to getConnection() will block for up to connectionTimeout milliseconds before timing out. Please read about pool sizing. Default: 10
Default: 10
minimumIdle
This property controls the minimum number of idle connections that HikariCP tries to maintain in the pool. If the idle connections dip below this value and total connections in the pool are less than maximumPoolSize, HikariCP will make a best effort to add additional connections quickly and efficiently. However, for maximum performance and responsiveness to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed size connection pool. Default: same as maximumPoolSize
Default: same as maximumPoolSize
maxLifetime
This property controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. The minimum allowed value is 30000ms (30 seconds). Default: 1800000 (30 minutes)
Default: 1800000 (30 minutes)
idleTimeout
This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This setting only applies when minimumIdle is defined to be less than maximumPoolSize. Idle connections will not be retired once the pool reaches minimumIdle connections. Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. The minimum allowed value is 10000ms (10 seconds). Default: 600000 (10 minutes)
Default: 600000 (10 minutes)
connectionTimeout
This property controls the maximum number of milliseconds that a client (that's you) will wait for a connection from the pool. If this time is exceeded without a connection becoming available, a SQLException will be thrown. Lowest acceptable connection timeout is 250 ms. Default: 30000 (30 seconds)
Default: 30000 (30 seconds)
Hikari 推荐 maxLifetime 设置为比数据库的 wait_timeout 时间少 30s 到 1min。如果你使用的是 mysql 数据库,可以使用 show global variables like ‘%timeout%’; 查看 wait_timeout,默认为 8 小时
注意有些公司使用的代理连接,具体wait_timeout可以咨询自己公司的运维组。
3.2 调整mysql-connector-java
升级MySQL jdbc driver到8.0.30,开启disableAbandonedConnectionCleanup
Oracle应该是接收了大量开发人员的反馈,在高版本中已经可以通过配置选择性关闭此功能。 mysql-connector-java 版本(8.0.22+)的代码对数据库连接的虚引用有新的处理方式,其增加了开关,可以手动关闭此功能。
其版本8.0.22介绍增加此参数即是为了解决JVM 虚引用相关问题,但是默认是未启用,需要手动开启:
https://dev.mysql.com/doc/relnotes/connector-j/8.0/en/news-8-0-22.html
When using Connector/J, the AbandonedConnectionCleanupThread thread can now be disabled completely by setting the new system property com.mysql.cj.disableAbandonedConnectionCleanup to true when configuring the JVM. The feature is for well-behaving applications that always close all connections they create. Thanks to Andrey Turbanov for contributing to the new feature. (Bug #30304764, Bug #96870)
有了这个配置,就可以在启动参数上设置属性:
java -jar app.jar -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
或者在代码里设置属性:
System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");
com.mysql.cj.disableAbandonedConnectionCleanup=true 时,生成数据库连接时就不会生成虚引用.