背景
在之前文章中写过 MySQL JDBC 驱动中的虚引用导致 JVM GC 耗时较长的问题,在驱动代码(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 类有个虚引用集合 connectionPhantomRefs 用于存储所有的数据库连接,NonRegisteringDriver.trackConnection 方法负责把新创建的连接放入集合,虚引用随着时间积累越来越多,导致 GC 时处理虚引用的耗时较长,影响了服务的吞吐量:
public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
...
NonRegisteringDriver.trackConnection(this);
...
}
public class NonRegisteringDriver implements Driver {
...
protected static final ConcurrentHashMap connectionPhantomRefs = new ConcurrentHashMap();
protected static void trackConnection(com.mysql.jdbc.Connection newConn) {
ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl)newConn, refQueue);
connectionPhantomRefs.put(phantomRef, phantomRef);
}
...
}
尝试减少数据库连接的生成速度,来降低虚引用的数量,但是效果并不理想。最终的解决方案是通过反射获取虚引用集合,利用定时任务来定期清理集合,避免 GC 处理虚引用耗时较长。
// 每两小时清理 connectionPhantomRefs,减少对 mixed GC 的影响
SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> {
try {
Field connectionPhantomRefs = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
connectionPhantomRefs.setAccessible(true);
Map map = (Map) connectionPhantomRefs.get(NonRegisteringDriver.class);
if (map.size() > 50) {
map.clear();
}
} catch (Exception e) {
log.error("connectionPhantomRefs clear error!", e);
}
}, 2, 2, TimeUnit.HOURS);
利用定时任务清理虚引用效果立竿见影,每日几亿请求的服务 mixed GC 耗时只有 10 - 30 毫秒左右,系统也很稳定,线上运行将近一年没有任何问题。
优化——暴力破解到优雅配置
最近又有同事遇到相同的问题,使用的 mysql-connector-java 版本与我们使用的版本一致,查看最新版本(8.0.32)的代码发现对数据库连接的虚引用有新的处理方式,不像老版本(5.1.38)中每一个连接都会生成虚引用,而是可以通过参数来控制是否需要生成。类 AbandonedConnectionCleanupThread 的相关代码如下:
//静态变量通过 System.getProperty 获取配置
private static boolean abandonedConnectionCleanupDisabled = Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");
public static boolean getBoolean(String name) {
return parseBoolean(System.getProperty(name));
}
protected static void trackConnection(MysqlConnection conn, NetworkResources io) {
//判断配置的属性值来决定是否需要生成虚引用
if (!abandonedConnectionCleanupDisabled) {
···
ConnectionFinalizerPhantomReference reference = new ConnectionFinalizerPhantomReference(conn, io, referenceQueue);
connectionFinalizerPhantomRefs.add(reference);
···
}
}
mysql-connector-java 的维护者应该是注意到了虚引用对 GC 的影响,所以优化了代码,让用户可以自定义虚引用的生成。
有了这个配置,就可以在启动参数上设置属性:
java -jar app.jar -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
或者在代码里设置属性:
System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");
当 com.mysql.cj.disableAbandonedConnectionCleanup=true 时,生成数据库连接时就不会生成虚引用,对 GC 就没有任何影响了。
建议还是使用第一种方式,通过启动参数配置更灵活一点。
什么是虚引用
有些读者看到这里知道 mysql-connector-java 生成的虚引用对 GC 有一些副作用,但是还不太了解虚引用到底是什么,有什么作用,这里我们在虚引用上做一点点拓展。
Java 虚引用(Phantom Reference)是Java中一种特殊的引用类型,它是最弱的一种引用。与其他引用不同,虚引用并不会影响对象的生命周期,也不会影响对象的垃圾回收。虚引用主要用于在对象被回收时收到系统通知,以便在回收时执行一些必要的清理工作。
上述虚引用的定义还是比较难理解,我们用代码来辅助理解:
先来生成一个虚引用:
//虚引用队列
ReferenceQueue