RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一種優(yōu)雅解決MySQL驅(qū)動(dòng)中虛引用導(dǎo)致GC耗時(shí)較長(zhǎng)問(wèn)題的方法

OSC開源社區(qū) ? 來(lái)源:DailyHappy ? 2023-12-20 09:52 ? 次閱讀

背景

在之前文章中寫過(guò) MySQL JDBC 驅(qū)動(dòng)中的虛引用導(dǎo)致 JVM GC 耗時(shí)較長(zhǎng)的問(wèn)題,在驅(qū)動(dòng)代碼(mysql-connector-java 5.1.38版本)中 NonRegisteringDriver 類有個(gè)虛引用集合 connectionPhantomRefs 用于存儲(chǔ)所有的數(shù)據(jù)庫(kù)連接,NonRegisteringDriver.trackConnection 方法負(fù)責(zé)把新創(chuàng)建的連接放入集合,虛引用隨著時(shí)間積累越來(lái)越多,導(dǎo)致 GC 時(shí)處理虛引用的耗時(shí)較長(zhǎng),影響了服務(wù)的吞吐量:

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ù)庫(kù)連接的生成速度,來(lái)降低虛引用的數(shù)量,但是效果并不理想。最終的解決方案是通過(guò)反射獲取虛引用集合,利用定時(shí)任務(wù)來(lái)定期清理集合,避免 GC 處理虛引用耗時(shí)較長(zhǎng)。

//每?jī)尚r(shí)清理connectionPhantomRefs,減少對(duì)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);

利用定時(shí)任務(wù)清理虛引用效果立竿見(jiàn)影,每日幾億請(qǐng)求的服務(wù) mixed GC 耗時(shí)只有 10 - 30 毫秒左右,系統(tǒng)也很穩(wěn)定,線上運(yùn)行將近一年沒(méi)有任何問(wèn)題。

優(yōu)化——暴力破解到優(yōu)雅配置

最近又有同事遇到相同的問(wèn)題,使用的 mysql-connector-java 版本與我們使用的版本一致,查看最新版本(8.0.32)的代碼發(fā)現(xiàn)對(duì)數(shù)據(jù)庫(kù)連接的虛引用有新的處理方式,不像老版本(5.1.38)中每一個(gè)連接都會(huì)生成虛引用,而是可以通過(guò)參數(shù)來(lái)控制是否需要生成。類 AbandonedConnectionCleanupThread 的相關(guān)代碼如下:

//靜態(tài)變量通過(guò)System.getProperty獲取配置
privatestaticbooleanabandonedConnectionCleanupDisabled=Boolean.getBoolean("com.mysql.cj.disableAbandonedConnectionCleanup");

publicstaticbooleangetBoolean(Stringname){
returnparseBoolean(System.getProperty(name));
}

protectedstaticvoidtrackConnection(MysqlConnectionconn,NetworkResourcesio){
//判斷配置的屬性值來(lái)決定是否需要生成虛引用
if(!abandonedConnectionCleanupDisabled){
···
ConnectionFinalizerPhantomReferencereference=newConnectionFinalizerPhantomReference(conn,io,referenceQueue);
connectionFinalizerPhantomRefs.add(reference);
···
}
}

mysql-connector-java 的維護(hù)者應(yīng)該是注意到了虛引用對(duì) GC 的影響,所以優(yōu)化了代碼,讓用戶可以自定義虛引用的生成。

有了這個(gè)配置,就可以在啟動(dòng)參數(shù)上設(shè)置屬性:

java-jarapp.jar-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true

或者在代碼里設(shè)置屬性:

System.setProperty(PropertyDefinitions.SYSP_disableAbandonedConnectionCleanup,"true");

當(dāng) com.mysql.cj.disableAbandonedConnectionCleanup=true 時(shí),生成數(shù)據(jù)庫(kù)連接時(shí)就不會(huì)生成虛引用,對(duì) GC 就沒(méi)有任何影響了。

建議還是使用第一種方式,通過(guò)啟動(dòng)參數(shù)配置更靈活一點(diǎn)。

什么是虛引用

有些讀者看到這里知道 mysql-connector-java 生成的虛引用對(duì) GC 有一些副作用,但是還不太了解虛引用到底是什么,有什么作用,這里我們?cè)谔撘蒙献鲆稽c(diǎn)點(diǎn)拓展。

Java 虛引用(Phantom Reference)是Java中一種特殊的引用類型,它是最弱的一種引用。與其他引用不同,虛引用并不會(huì)影響對(duì)象的生命周期,也不會(huì)影響對(duì)象的垃圾回收。虛引用主要用于在對(duì)象被回收時(shí)收到系統(tǒng)通知,以便在回收時(shí)執(zhí)行一些必要的清理工作。

上述虛引用的定義還是比較難理解,我們用代碼來(lái)輔助理解:

先來(lái)生成一個(gè)虛引用:

//虛引用隊(duì)列
ReferenceQueuequeue=newReferenceQueue<>();
//關(guān)聯(lián)對(duì)象
Objecto=newObject();
//調(diào)用構(gòu)造方法生成一個(gè)虛引用第一個(gè)參數(shù)就是關(guān)聯(lián)對(duì)象第二個(gè)參數(shù)是關(guān)聯(lián)隊(duì)列
PhantomReferencephantomReference=newPhantomReference<>(o,queue);
//執(zhí)行垃圾回收
System.gc();
//延時(shí)確?;厥胀戤?Thread.sleep(100L);
//當(dāng)Objecto被回收時(shí)可以從虛引用隊(duì)列里獲取到與之關(guān)聯(lián)的虛引用這里就是phantomReference這個(gè)對(duì)象
Referencepoll=queue.poll();

虛引用的構(gòu)造方法需要兩個(gè)入?yún)?,第一個(gè)就是關(guān)聯(lián)的對(duì)象、第二個(gè)是虛引用隊(duì)列 ReferenceQueue。虛引用需要和 ReferenceQueue 配合使用,當(dāng)對(duì)象 Object o 被垃圾回收時(shí),與 Object o 關(guān)聯(lián)的虛引用就會(huì)被放入到 ReferenceQueue 中。通過(guò)從 ReferenceQueue 中是否存在虛引用來(lái)判斷對(duì)象是否被回收。

我們?cè)賮?lái)理解上面對(duì)虛引用的定義,虛引用不會(huì)影響對(duì)象的生命周期,也不會(huì)影響對(duì)象的垃圾回收。如果上述代碼里的phantomReference 是一個(gè)普通的對(duì)象,那么在執(zhí)行 System.gc() 時(shí) Object o 一定不會(huì)被回收掉,因?yàn)槠胀▽?duì)象持有 Object o 的強(qiáng)引用,還不會(huì)被作為垃圾。這里的 phantomReference 是一個(gè)虛引用的話 Object o 就會(huì)被直接回收掉。然后會(huì)將關(guān)聯(lián)的虛引用放到隊(duì)列里,這就是虛引用關(guān)聯(lián)對(duì)象被回收時(shí)會(huì)收到系統(tǒng)通知的機(jī)制。

一些實(shí)踐能力很強(qiáng)的讀者會(huì)復(fù)制上述代碼去運(yùn)行,發(fā)現(xiàn)垃圾回收之后隊(duì)列里并沒(méi)有虛引用。這是因?yàn)?Object o 還在棧里,屬于是 GC Root 的一種,不會(huì)被垃圾回收。我們可以這樣改寫:

staticReferenceQueuequeue=newReferenceQueue<>();

publicstaticvoidmain(String[]args)throwsInterruptedException{
PhantomReferencephantomReference=buildReference();
System.gc();Thread.sleep(100);
System.out.println(queue.poll());
}

publicstaticPhantomReferencebuildReference(){
Objecto=newObject();
returnnewPhantomReference<>(o,queue);
}

不在 main 方法里實(shí)例化關(guān)聯(lián)對(duì)象 Object o,而是利用一個(gè) buildReference 方法來(lái)實(shí)例化,這樣在執(zhí)行垃圾回收的時(shí)候,Object o 已經(jīng)出棧了,不再是 GC Root,會(huì)被當(dāng)做垃圾來(lái)回收。這樣就能從虛引用隊(duì)列里取出關(guān)聯(lián)的虛引用進(jìn)行后續(xù)處理。

關(guān)聯(lián)對(duì)象真的被回收了嗎

執(zhí)行完垃圾回收之后,我們確實(shí)能從虛引用隊(duì)列里獲取到虛引用了,我們可以思考一下,與該虛引用關(guān)聯(lián)的對(duì)象真的已經(jīng)被回收了嗎?

使用一個(gè)小實(shí)驗(yàn)來(lái)探索答案:

publicstaticvoidmain(String[]args){
ReferenceQueuequeue=newReferenceQueue<>();
PhantomReferencephantomReference=newPhantomReference<>(
newbyte[1024*1024*2],queue);
System.gc();Thread.sleep(100L);
System.out.println(queue.poll());
byte[]bytes=newbyte[1024*1024*4];
}

代碼里生成一個(gè)虛引用,關(guān)聯(lián)對(duì)象是一個(gè)大小為 2M 的數(shù)組,執(zhí)行垃圾回收之后嘗試再實(shí)例化一個(gè)大小為 4M 的數(shù)組。如果我們從虛引用隊(duì)列里獲取到虛引用的時(shí)候關(guān)聯(lián)對(duì)象已經(jīng)被回收,那么就能正常申請(qǐng)到 4M 的數(shù)組。(設(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)

從輸出可以看到,申請(qǐng) 4M 內(nèi)存的時(shí)候內(nèi)存溢出,那么問(wèn)題的答案就很明顯了,關(guān)聯(lián)對(duì)象并沒(méi)有被真正的回收,內(nèi)存也沒(méi)有被釋放。

再做一點(diǎn)小小的改造,實(shí)例化新數(shù)組的之前將虛引用直接置為 null,這樣關(guān)聯(lián)對(duì)象就能被真正的回收掉,也能申請(qǐng)足夠的內(nèi)存:

publicstaticvoidmain(String[]args){
ReferenceQueuequeue=newReferenceQueue<>();
PhantomReferencephantomReference=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];
}

如果我們使用了虛引用,但是沒(méi)有及時(shí)清理虛引用的話可能會(huì)導(dǎo)致內(nèi)存泄露

虛引用的使用場(chǎng)景——mysql-connector-java 虛引用源碼分析

讀到這里相信你已經(jīng)了解了虛引用的一些基本情況,那么它的使用場(chǎng)景在哪里呢?

最典型的場(chǎng)景就是最開始寫到的 mysql-connector-java 里處理 MySQL 連接的兜底邏輯。用虛引用來(lái)包裝 MySQL 連接,如果一個(gè)連接對(duì)象被回收的時(shí)候,會(huì)從虛引用隊(duì)列里收到通知,如果有些連接沒(méi)有被正確關(guān)閉的話,就會(huì)在回收之前進(jìn)行連接關(guān)閉的操作。

從 mysql-connector-java 的 AbandonedConnectionCleanupThread 類代碼中可以發(fā)現(xiàn)并沒(méi)有使用原生的 PhantomReference 對(duì)象,而是使用的是包裝過(guò)的 ConnectionFinalizerPhantomReference,增加了一個(gè)屬性 NetworkResources,這是為了方便從虛引用隊(duì)列中的虛引用上獲取到需要處理的資源。包裝類中還有一個(gè) finalizeResources 方法,用來(lái)關(guān)閉網(wǎng)絡(luò)連接:

privatestaticclassConnectionFinalizerPhantomReferenceextendsPhantomReference{
//放置需要GC后后置處理的網(wǎng)絡(luò)資源
privateNetworkResourcesnetworkResources;
ConnectionFinalizerPhantomReference(MysqlConnectionconn,NetworkResourcesnetworkResources,ReferenceQueuerefQueue){
super(conn,refQueue);
this.networkResources=networkResources;
}
voidfinalizeResources(){
if(this.networkResources!=null){
try{
this.networkResources.forceClose();
}finally{
this.networkResources=null;
}
}
}
}

AbandonedConnectionCleanupThread 實(shí)現(xiàn)了 Runnable 接口,在 run 方法里循環(huán)讀取虛引用隊(duì)列 referenceQueue 里的虛引用,然后調(diào)用 finalizeResource 方法來(lái)進(jìn)行后置的處理,避免連接泄露:

publicvoidrun(){
while(true){
try{
...
Referencereference=referenceQueue.remove(5000L);
if(reference!=null){
//強(qiáng)轉(zhuǎn)為ConnectionFinalizerPhantomReference
finalizeResource((ConnectionFinalizerPhantomReference)reference);
}
...
}
}
}

privatestaticvoidfinalizeResource(ConnectionFinalizerPhantomReferencereference){
try{
//兜底處理網(wǎng)絡(luò)資源
reference.finalizeResources();
reference.clear();
}finally{
//移除虛引用避免可能造成的內(nèi)存溢出
connectionFinalizerPhantomRefs.remove(reference);
}
}

如果你希望在某些對(duì)象被回收的時(shí)候做一些后置工作,可以參考 mysql-connector-java 中的一些實(shí)現(xiàn)邏輯。

總結(jié)

本文簡(jiǎn)述了一種優(yōu)雅解決 MySQL 驅(qū)動(dòng)中虛引用導(dǎo)致 GC 耗時(shí)較長(zhǎng)問(wèn)題的解決方法、也根據(jù)自己的理解講述了虛引用的作用、結(jié)合 MySQL 驅(qū)動(dòng)的源碼描述了虛引用的使用場(chǎng)景,希望對(duì)你能有所幫助。






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2966

    瀏覽量

    104702
  • MySQL
    +關(guān)注

    關(guān)注

    1

    文章

    804

    瀏覽量

    26528
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    158

    瀏覽量

    12220

原文標(biāo)題:MySQL驅(qū)動(dòng)中虛引用GC耗時(shí)優(yōu)化與源碼分析

文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    步進(jìn)電機(jī)5驅(qū)動(dòng)方法的利弊分析

    這5驅(qū)動(dòng)方法的利弊,以選擇合適的驅(qū)動(dòng)方法來(lái)做電機(jī)驅(qū)動(dòng)。1. 恒電壓
    發(fā)表于 01-27 14:45

    C++服務(wù)編譯耗時(shí)優(yōu)化的原理和服務(wù)分析

    編譯的耗時(shí)。通過(guò)這個(gè)方式能夠找到各個(gè)文件編譯耗時(shí)的共性,下圖是編譯展開后文件大小截圖。3.2 頭文件依賴分析頭文件依賴分析是從引用頭文件數(shù)量的角度來(lái)看代碼是否合理的一種分析方式,我們實(shí)
    發(fā)表于 12-23 17:32

    一種優(yōu)雅的方式去實(shí)現(xiàn)個(gè)Verilog版的狀態(tài)機(jī)

    描述:基于此,我們便可以方便快捷的去描述狀態(tài)機(jī),以一種優(yōu)雅的方式去實(shí)現(xiàn)狀態(tài)機(jī)描述,而對(duì)于他人閱讀來(lái)講也是相當(dāng)OK的。等等,還有更好玩兒的。在SpinalHDL里,定義了四可以聲明狀態(tài)的類型
    發(fā)表于 07-13 14:56

    解密方舟的高性能內(nèi)存回收技術(shù)——HPP GC

    和高性能,HPP GC采取了兩項(xiàng)優(yōu)化措施:措施:在新增引用關(guān)系時(shí)增加標(biāo)記屏障(Marking Barrier),以確保標(biāo)記結(jié)果的正確性。并發(fā)標(biāo)記過(guò)程,JS線程有可能會(huì)更改對(duì)象之間的
    發(fā)表于 07-20 10:44

    什么是PCBA焊?解決PCBA焊的方法介紹

    層。它們沒(méi)有完全接觸在起。肉眼般無(wú)法看出其狀態(tài)。 但是其電氣特性并沒(méi)有導(dǎo)通或?qū)ú涣?。影響電路特性?! CBA焊是常見(jiàn)的一種線路故障,有兩
    發(fā)表于 04-06 16:25

    一種有效的視頻序列拼接方法

    針對(duì)視頻序列拼接容易造成拼接耗時(shí)較長(zhǎng)、拼接效果不佳等問(wèn)題,提出一種有效的視頻序列拼接方法,首先,利用時(shí)域檢測(cè)窗口對(duì)視頻序列進(jìn)行關(guān)鍵幀的提取
    發(fā)表于 09-03 16:24 ?30次下載

    一種帶有均衡策略的無(wú)通道容錯(cuò)路由算法

    一種帶有均衡策略的無(wú)通道容錯(cuò)路由算法
    發(fā)表于 01-07 20:49 ?0次下載

    DSP硬件驅(qū)動(dòng)程序的一種方法

    DSP硬件驅(qū)動(dòng)程序的一種方法
    發(fā)表于 10-19 10:48 ?1次下載
    DSP硬件<b class='flag-5'>驅(qū)動(dòng)</b>程序的<b class='flag-5'>一種方法</b>

    "引用"在Android和Java的工作原理

    本文講的是徹底理解引用在Android和Java的工作原理,引用指向了個(gè)對(duì)象,你能通過(guò)引用訪問(wèn)對(duì)象。Java默認(rèn)有4
    發(fā)表于 11-27 08:55 ?1265次閱讀
    "<b class='flag-5'>引用</b>"在Android和Java<b class='flag-5'>中</b>的工作原理

    基于shared_ptr的C++非侵入式引用計(jì)數(shù)解決方案的缺陷

    嚴(yán)格地說(shuō),引用計(jì)數(shù)其實(shí)也是一種最樸素的GC。相對(duì)于現(xiàn)代的GC技術(shù),引用計(jì)數(shù)的實(shí)現(xiàn)簡(jiǎn)單,但相應(yīng)地,它也存在著循環(huán)
    的頭像 發(fā)表于 06-13 14:24 ?1388次閱讀

    次JVM GC長(zhǎng)暫停的排查過(guò)程

    在高并發(fā)下,Java 程序的 GC 問(wèn)題屬于很典型的類問(wèn)題,帶來(lái)的影響往往會(huì)被進(jìn)步放大。不管是「GC 頻率過(guò)快」還是「GC
    的頭像 發(fā)表于 01-17 10:08 ?603次閱讀

    運(yùn)放電路為什么會(huì)出現(xiàn)短和斷?

    運(yùn)放電路為什么會(huì)出現(xiàn)短和斷?? 運(yùn)放電路是電子電路中常用的一種放大電路,可以實(shí)現(xiàn)信號(hào)放大和信號(hào)濾波等功能。然而在實(shí)際應(yīng)用,經(jīng)常會(huì)出現(xiàn)
    的頭像 發(fā)表于 09-20 16:29 ?4702次閱讀

    php的mysql無(wú)法啟動(dòng)

    MySQL一種常用的關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),而PHP是一種廣泛應(yīng)用于服務(wù)器端的腳本語(yǔ)言。在使用PHP開發(fā)網(wǎng)站或應(yīng)用時(shí),經(jīng)常會(huì)碰到MySQL無(wú)法啟動(dòng)的問(wèn)題。本文將詳細(xì)介紹解決
    的頭像 發(fā)表于 12-04 15:59 ?1572次閱讀

    導(dǎo)致MySQL索引失效的情況以及相應(yīng)的解決方法

    導(dǎo)致MySQL索引失效的情況以及相應(yīng)的解決方法? MySQL索引的目的是提高查詢效率,但有些情況下索引可能會(huì)失效,導(dǎo)致查詢變慢或效果不如預(yù)期
    的頭像 發(fā)表于 12-28 10:01 ?752次閱讀

    柵極驅(qū)動(dòng)ic焊會(huì)燒嗎

    柵極驅(qū)動(dòng)IC焊是否會(huì)導(dǎo)致燒毀,這個(gè)問(wèn)題涉及到多個(gè)因素,包括焊的嚴(yán)重程度、工作環(huán)境條件以及柵極驅(qū)動(dòng)IC本身的特性等。以下是對(duì)這
    的頭像 發(fā)表于 09-18 09:26 ?327次閱讀
    RM新时代网站-首页