背景
在之前文章中写过 MySQL JDBC 驱动中的虚引用导致 JVM GC 耗时较长的问题,在驱动代码(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 类有个虚引用集合 connectionPhantomRefs 用于存储所有的数据库连接,NonRegisteringDriver.trackConnection 方法负责把新创建的连接放入集合,虚引用随着时间积累越来越多,导致 GC 时处理虚引用的耗时较长,影响了服务的吞吐量:
publicConnectionImpl(StringhostToConnectTo,intportToConnectTo,Propertiesinfo,StringdatabaseToConnectTo,Stringurl)throwsSQLException{
...
NonRegisteringDriver.trackConnection(this);
...
}
publicclassNonRegisteringDriverimplementsDriver{
...
protectedstaticfinalConcurrentHashMapconnectionPhantomRefs=newConcurrentHashMap();
protectedstaticvoidtrackConnection(com.mysql.jdbc.ConnectionnewConn){
ConnectionPhantomReferencephantomRef=newConnectionPhantomReference((ConnectionImpl)newConn,refQueue);
connectionPhantomRefs.put(phantomRef,phantomRef);
}
...
}
尝试减少数据库连接的生成速度,来降低虚引用的数量,但是效果并不理想。最终的解决方案是通过反射获取虚引用集合,利用定时任务来定期清理集合,避免 GC 处理虚引用耗时较长。
//每两小时清理connectionPhantomRefs,减少对mixedGC的影响
SCHEDULED_EXECUTOR.scheduleAtFixedRate(()->{
try{
FieldconnectionPhantomRefs=NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
connectionPhantomRefs.setAccessible(true);
Mapmap=(Map)connectionPhantomRefs.get(NonRegisteringDriver.class);
if(map.size()>50){
map.clear();
}
}catch(Exceptione){
log.error("connectionPhantomRefsclearerror!",e);
}
},2,2,TimeUnit.HOURS);
利用定时任务清理虚引用效果立竿见影,每日几亿请求的服务 mixed GC 耗时只有 10 - 30 毫秒左右,系统也很稳定,线上运行将近一年没有任何问题。
优化——暴力破解到优雅配置
最近又有同事遇到相同的问题,使用的 mysql-connector-java 版本与我们使用的版本一致,查看最新版本(8.0.32)的代码发现对数据库连接的虚引用有新的处理方式,不像老版本(5.1.38)中每一个连接都会生成虚引用,而是可以通过参数来控制是否需要生成。类 AbandonedConnectionCleanupThread 的相关代码如下:
//静态变量通过System.getProperty获取配置
privatestaticbooleanabandonedConnectionCleanupDisabled=Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");
publicstaticbooleangetBoolean(Stringname){
returnparseBoolean(System.getProperty(name));
}
protectedstaticvoidtrackConnection(MysqlConnectionconn,NetworkResourcesio){
//判断配置的属性值来决定是否需要生成虚引用
if(!abandonedConnectionCleanupDisabled){
···
ConnectionFinalizerPhantomReferencereference=newConnectionFinalizerPhantomReference(conn,io,referenceQueue);
connectionFinalizerPhantomRefs.add(reference);
···
}
}
mysql-connector-java 的维护者应该是注意到了虚引用对 GC 的影响,所以优化了代码,让用户可以自定义虚引用的生成。
有了这个配置,就可以在启动参数上设置属性:
java-jarapp.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