背景
在之前文章中寫過 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