背景
在之前文章中寫過 MySQL JDBC 驅(qū)動中的虛引用導致 JVM GC 耗時較長的問題,在驅(qū)動代碼(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 類有個虛引用集合 connectionPhantomRefs 用于存儲所有的數(shù)據(jù)庫連接,NonRegisteringDriver.trackConnection 方法負責把新創(chuàng)建的連接放入集合,虛引用隨著時間積累越來越多,導致 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); } ... }
嘗試減少數(shù)據(jù)庫連接的生成速度,來降低虛引用的數(shù)量,但是效果并不理想。最終的解決方案是通過反射獲取虛引用集合,利用定時任務來定期清理集合,避免 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 毫秒左右,系統(tǒng)也很穩(wěn)定,線上運行將近一年沒有任何問題。
優(yōu)化——暴力破解到優(yōu)雅配置
最近又有同事遇到相同的問題,使用的 mysql-connector-java 版本與我們使用的版本一致,查看最新版本(8.0.32)的代碼發(fā)現(xiàn)對數(shù)據(jù)庫連接的虛引用有新的處理方式,不像老版本(5.1.38)中每一個連接都會生成虛引用,而是可以通過參數(shù)來控制是否需要生成。類 AbandonedConnectionCleanupThread 的相關代碼如下:
//靜態(tài)變量通過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 的影響,所以優(yōu)化了代碼,讓用戶可以自定義虛引用的生成。
有了這個配置,就可以在啟動參數(shù)上設置屬性:
java-jarapp.jar-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
或者在代碼里設置屬性:
System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");
當 com.mysql.cj.disableAbandonedConnectionCleanup=true 時,生成數(shù)據(jù)庫連接時就不會生成虛引用,對 GC 就沒有任何影響了。
建議還是使用第一種方式,通過啟動參數(shù)配置更靈活一點。
什么是虛引用
有些讀者看到這里知道 mysql-connector-java 生成的虛引用對 GC 有一些副作用,但是還不太了解虛引用到底是什么,有什么作用,這里我們在虛引用上做一點點拓展。
Java 虛引用(Phantom Reference)是Java中一種特殊的引用類型,它是最弱的一種引用。與其他引用不同,虛引用并不會影響對象的生命周期,也不會影響對象的垃圾回收。虛引用主要用于在對象被回收時收到系統(tǒng)通知,以便在回收時執(zhí)行一些必要的清理工作。
上述虛引用的定義還是比較難理解,我們用代碼來輔助理解:
先來生成一個虛引用:
//虛引用隊列 ReferenceQueue
虛引用的構造方法需要兩個入?yún)ⅲ谝粋€就是關聯(lián)的對象、第二個是虛引用隊列 ReferenceQueue。虛引用需要和 ReferenceQueue 配合使用,當對象 Object o 被垃圾回收時,與 Object o 關聯(lián)的虛引用就會被放入到 ReferenceQueue 中。通過從 ReferenceQueue 中是否存在虛引用來判斷對象是否被回收。
我們再來理解上面對虛引用的定義,虛引用不會影響對象的生命周期,也不會影響對象的垃圾回收。如果上述代碼里的phantomReference 是一個普通的對象,那么在執(zhí)行 System.gc() 時 Object o 一定不會被回收掉,因為普通對象持有 Object o 的強引用,還不會被作為垃圾。這里的 phantomReference 是一個虛引用的話 Object o 就會被直接回收掉。然后會將關聯(lián)的虛引用放到隊列里,這就是虛引用關聯(lián)對象被回收時會收到系統(tǒng)通知的機制。
一些實踐能力很強的讀者會復制上述代碼去運行,發(fā)現(xiàn)垃圾回收之后隊列里并沒有虛引用。這是因為 Object o 還在棧里,屬于是 GC Root 的一種,不會被垃圾回收。我們可以這樣改寫:
staticReferenceQueuequeue=newReferenceQueue<>(); publicstaticvoidmain(String[]args)throwsInterruptedException{ PhantomReference phantomReference=buildReference(); System.gc();Thread.sleep(100); System.out.println(queue.poll()); } publicstaticPhantomReference buildReference(){ Objecto=newObject(); returnnewPhantomReference<>(o,queue); }
不在 main 方法里實例化關聯(lián)對象 Object o,而是利用一個 buildReference 方法來實例化,這樣在執(zhí)行垃圾回收的時候,Object o 已經(jīng)出棧了,不再是 GC Root,會被當做垃圾來回收。這樣就能從虛引用隊列里取出關聯(lián)的虛引用進行后續(xù)處理。
關聯(lián)對象真的被回收了嗎
執(zhí)行完垃圾回收之后,我們確實能從虛引用隊列里獲取到虛引用了,我們可以思考一下,與該虛引用關聯(lián)的對象真的已經(jīng)被回收了嗎?
使用一個小實驗來探索答案:
publicstaticvoidmain(String[]args){ ReferenceQueuequeue=newReferenceQueue<>(); PhantomReference phantomReference=newPhantomReference<>( newbyte[1024*1024*2],queue); System.gc();Thread.sleep(100L); System.out.println(queue.poll()); byte[]bytes=newbyte[1024*1024*4]; }
代碼里生成一個虛引用,關聯(lián)對象是一個大小為 2M 的數(shù)組,執(zhí)行垃圾回收之后嘗試再實例化一個大小為 4M 的數(shù)組。如果我們從虛引用隊列里獲取到虛引用的時候關聯(lián)對象已經(jīng)被回收,那么就能正常申請到 4M 的數(shù)組。(設置堆內(nèi)存大小為 5M -Xmx5m -Xms5m)
執(zhí)行代碼輸出如下:
java.lang.ref.PhantomReference@533ddba Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspace atcom.ppphuang.demo.phantomReference.PhantomReferenceDemo.main(PhantomReferenceDemo.java:15)
從輸出可以看到,申請 4M 內(nèi)存的時候內(nèi)存溢出,那么問題的答案就很明顯了,關聯(lián)對象并沒有被真正的回收,內(nèi)存也沒有被釋放。
再做一點小小的改造,實例化新數(shù)組的之前將虛引用直接置為 null,這樣關聯(lián)對象就能被真正的回收掉,也能申請足夠的內(nèi)存:
publicstaticvoidmain(String[]args){ ReferenceQueuequeue=newReferenceQueue<>(); PhantomReference phantomReference=newPhantomReference<>( newbyte[1024*1024*2],queue); System.gc();Thread.sleep(100L); System.out.println(queue.poll()); //虛引用直接置為null phantomReference=null; byte[]bytes=newbyte[1024*1024*4]; }
如果我們使用了虛引用,但是沒有及時清理虛引用的話可能會導致內(nèi)存泄露。
虛引用的使用場景——mysql-connector-java 虛引用源碼分析
讀到這里相信你已經(jīng)了解了虛引用的一些基本情況,那么它的使用場景在哪里呢?
最典型的場景就是最開始寫到的 mysql-connector-java 里處理 MySQL 連接的兜底邏輯。用虛引用來包裝 MySQL 連接,如果一個連接對象被回收的時候,會從虛引用隊列里收到通知,如果有些連接沒有被正確關閉的話,就會在回收之前進行連接關閉的操作。
從 mysql-connector-java 的 AbandonedConnectionCleanupThread 類代碼中可以發(fā)現(xiàn)并沒有使用原生的 PhantomReference 對象,而是使用的是包裝過的 ConnectionFinalizerPhantomReference,增加了一個屬性 NetworkResources,這是為了方便從虛引用隊列中的虛引用上獲取到需要處理的資源。包裝類中還有一個 finalizeResources 方法,用來關閉網(wǎng)絡連接:
privatestaticclassConnectionFinalizerPhantomReferenceextendsPhantomReference{ //放置需要GC后后置處理的網(wǎng)絡資源 privateNetworkResourcesnetworkResources; ConnectionFinalizerPhantomReference(MysqlConnectionconn,NetworkResourcesnetworkResources,ReferenceQueue?super?MysqlConnection>refQueue){ super(conn,refQueue); this.networkResources=networkResources; } voidfinalizeResources(){ if(this.networkResources!=null){ try{ this.networkResources.forceClose(); }finally{ this.networkResources=null; } } } }
AbandonedConnectionCleanupThread 實現(xiàn)了 Runnable 接口,在 run 方法里循環(huán)讀取虛引用隊列 referenceQueue 里的虛引用,然后調(diào)用 finalizeResource 方法來進行后置的處理,避免連接泄露:
publicvoidrun(){ while(true){ try{ ... Reference?extends?MysqlConnection>reference=referenceQueue.remove(5000L); if(reference!=null){ //強轉(zhuǎn)為ConnectionFinalizerPhantomReference finalizeResource((ConnectionFinalizerPhantomReference)reference); } ... } } } privatestaticvoidfinalizeResource(ConnectionFinalizerPhantomReferencereference){ try{ //兜底處理網(wǎng)絡資源 reference.finalizeResources(); reference.clear(); }finally{ //移除虛引用避免可能造成的內(nèi)存溢出 connectionFinalizerPhantomRefs.remove(reference); } }
如果你希望在某些對象被回收的時候做一些后置工作,可以參考 mysql-connector-java 中的一些實現(xiàn)邏輯。
總結
本文簡述了一種優(yōu)雅解決 MySQL 驅(qū)動中虛引用導致 GC 耗時較長問題的解決方法、也根據(jù)自己的理解講述了虛引用的作用、結合 MySQL 驅(qū)動的源碼描述了虛引用的使用場景,希望對你能有所幫助。
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2975瀏覽量
105150 -
MySQL
+關注
關注
1文章
829瀏覽量
26743 -
JVM
+關注
關注
0文章
158瀏覽量
12261
原文標題:MySQL驅(qū)動中虛引用GC耗時優(yōu)化與源碼分析
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關推薦
C++服務編譯耗時優(yōu)化的原理和服務分析
以一種更優(yōu)雅的方式去實現(xiàn)一個Verilog版的狀態(tài)機
什么是PCBA虛焊?解決PCBA虛焊的方法介紹
一種有效的視頻序列拼接方法
"引用"在Android和Java中的工作原理
!["<b class='flag-5'>引用</b>"在Android和Java<b class='flag-5'>中</b>的工作原理](https://file1.elecfans.com//web2/M00/A6/F7/wKgZomUMQYeARnjsAABbK9YRHoU213.png)
評論