<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          MySQL 驅(qū)動(dòng)中虛引用 GC 耗時(shí)優(yōu)化與源碼分析

          共 8605字,需瀏覽 18分鐘

           ·

          2023-05-21 23:27

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

          • 虛引用的作用與使用場(chǎng)景

          • MySQL 驅(qū)動(dòng)源碼中的虛引用分析

          背景

          在之前文章中寫(xiě)過(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ù)的吞吐量:

                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<ConnectionPhantomReference,?ConnectionPhantomReference>?connectionPhantomRefs?=?new?ConcurrentHashMap();
          ???
          ??protected?static?void?trackConnection(com.mysql.jdbc.Connection?newConn)?{
          ????????ConnectionPhantomReference?phantomRef?=?new?ConnectionPhantomReference((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ì)?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);

          利用定時(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?獲取配置
          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)?{
          ????//判斷配置的屬性值來(lái)決定是否需要生成虛引用
          ??????if?(!abandonedConnectionCleanupDisabled)?{
          ?????????···
          ??????????ConnectionFinalizerPhantomReference?reference?=?new?ConnectionFinalizerPhantomReference(conn,?io,?referenceQueue);
          ??????????connectionFinalizerPhantomRefs.add(reference);
          ?????????···?
          ??????}
          ??}

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

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

                java?-jar?app.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ì)列
          ReferenceQueue<Object>?queue?=?new?ReferenceQueue<>();
          //關(guān)聯(lián)對(duì)象
          Object?o?=?new?Object();
          //調(diào)用構(gòu)造方法生成一個(gè)虛引用?第一個(gè)參數(shù)就是關(guān)聯(lián)對(duì)象?第二個(gè)參數(shù)是關(guān)聯(lián)隊(duì)列
          PhantomReference<Object>?phantomReference?=?new?PhantomReference<>(o,?queue);
          //執(zhí)行垃圾回收
          System.gc();
          //延時(shí)確保回收完畢
          Thread.sleep(100L);
          //當(dāng)?Object?o?被回收時(shí)可以從虛引用隊(duì)列里獲取到與之關(guān)聯(lián)的虛引用?這里就是?phantomReference?這個(gè)對(duì)象
          Reference<?>?poll?=?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ì)被垃圾回收。我們可以這樣改寫(xiě):

                static?ReferenceQueue<Object>?queue?=?new?ReferenceQueue<>();

          public?static?void?main(String[]?args)?throws?InterruptedException?{
          ????PhantomReference<Object>?phantomReference?=?buildReference();
          ????System.gc();Thread.sleep(100);
          ????System.out.println(queue.poll());
          }

          public?static?PhantomReference<Object>?buildReference()?{
          ????Object?o?=?new?Object();
          ????return?new?PhantomReference<>(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)探索答案:

                public?static?void?main(String[]?args)?{
          ??????ReferenceQueue<byte[]>?queue?=?new?ReferenceQueue<>();
          ??????PhantomReference<byte[]>?phantomReference?=?new?PhantomReference<>(
          ??????????????new?byte[1024?*?1024?*?2],?queue);
          ??????System.gc();Thread.sleep(100L);
          ??????System.out.println(queue.poll());
          ??????byte[]?bytes?=?new?byte[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
          Exception?in?thread?"main"?java.lang.OutOfMemoryError:?Java?heap?space
          ?at?com.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)存:

                public?static?void?main(String[]?args)?{
          ??????ReferenceQueue<byte[]>?queue?=?new?ReferenceQueue<>();
          ??????PhantomReference<byte[]>?phantomReference?=?new?PhantomReference<>(
          ??????????????new?byte[1024?*?1024?*?2],?queue);
          ??????System.gc();Thread.sleep(100L);
          ??????System.out.println(queue.poll());
          ????//虛引用直接置為?null
          ????phantomReference?=?null;
          ??????byte[]?bytes?=?new?byte[1024?*?1024?*?4];
          ??}

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

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

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

          最典型的場(chǎng)景就是最開(kāi)始寫(xiě)到的 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ò)連接:

                private?static?class?ConnectionFinalizerPhantomReference?extends?PhantomReference<MysqlConnection>?{
          ??????//放置需要GC后后置處理的網(wǎng)絡(luò)資源
          ??????private?NetworkResources?networkResources;
          ??????ConnectionFinalizerPhantomReference(MysqlConnection?conn,?NetworkResources?networkResources,?ReferenceQueue<??super?MysqlConnection>?refQueue)?{
          ??????????super(conn,?refQueue);
          ??????????this.networkResources?=?networkResources;
          ??????}
          ??????void?finalizeResources()?{
          ??????????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)行后置的處理,避免連接泄露:

                public?void?run()?{
          ????while(true)?{
          ????????try?{
          ???????????...
          ????????????Reference<??extends?MysqlConnection>?reference?=?referenceQueue.remove(5000L);
          ????????????if?(reference?!=?null)?{
          ???????????????//強(qiáng)轉(zhuǎn)為?ConnectionFinalizerPhantomReference
          ????????????????finalizeResource((ConnectionFinalizerPhantomReference)reference);
          ????????????}
          ???????????...
          ????????}
          ????}
          }

          private?static?void?finalizeResource(ConnectionFinalizerPhantomReference?reference)?{
          ????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ì)你能有所幫助。

          瀏覽 79
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  99这里是精品 | 欧美成人性爱在线视频免费 | www.操逼| 亚洲欧美日韩激情 | 91亚洲视频 |