<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>

          由JDK bug引發(fā)的線上OOM

          共 4535字,需瀏覽 10分鐘

           ·

          2023-09-01 01:21

          由JDK bug引發(fā)的線上OOM

          最近生產(chǎn)環(huán)境的一個應(yīng)用忽然發(fā)生了OOM,還好是業(yè)務(wù)低峰期,沒有導(dǎo)致什么嚴重問題,下面記錄下本次排查的過程;

          故障臨時處理

          在某天下午,正在愉快的寫代碼時,忽然看到業(yè)務(wù)反饋支付服務(wù)不能用的消息,因為最近沒有發(fā)布,所以感覺不會是什么大事,十有八九是網(wǎng)絡(luò)波動啥的,畢竟之前遇到過好多次,那剩下的就是找證據(jù)了,先看看日志,有沒有報錯(暫時還未接告警,所以要人肉看),結(jié)果不看不要緊,一看嚇一跳,日志密密麻麻全都是OOM報錯

          5f8c05a91c1f82d3108920761e39e9f0.webp

          幸好作者這百年Java開發(fā)經(jīng)驗不是白給的,反手就是一個重啟服務(wù),雖然看起來只是一個簡單的重啟,但是操作起來可并不簡單,里邊的道道還是很多的,重啟的時候要注意留一臺作為現(xiàn)場保護起來,同時給這臺保留現(xiàn)場的實例的流量摘除掉,然后給其他實例重啟起來,這樣用戶就能正常使用了;

          本來以為事情到這里就結(jié)束了,但是就當我準備繼續(xù)下一步的時候,我發(fā)現(xiàn)重啟的早的那臺機器內(nèi)存已經(jīng)又直線上升上來了;

          caad57f830057c8cf243029f83689b5d.webp

          不過這也難不倒我,既然重啟后服務(wù)內(nèi)存又開始飆升,說明肯定是定時任務(wù)、批處理之類的觸發(fā)了,查看了下日志,果然是有大批的定時任務(wù)在執(zhí)行,將定時任務(wù)暫停后發(fā)現(xiàn)就好了,下面開始對現(xiàn)場進行分析;

          現(xiàn)場處理

          登錄到我們保留的現(xiàn)場機器上,使用下面的命令執(zhí)行堆dump,方便我們后續(xù)分析:

          # 安裝gdb,如果機器上有就無需安裝
          yum install -y gdb

          # 設(shè)置不限制core dump大小
          ulimit -c unlimited

          # 生成core dump,文件名叫core,也可以自己起名,100是目標Java進程pid,這個需要根據(jù)實際的來,命令執(zhí)行完畢后會生成一個core.100的core dump
          gcore 100 -o core

          有的同學(xué)可能看到這里就開始迷糊了,Java堆dump不是用jmap命令嗎,上邊的命令跟jmap也沒什么關(guān)系呀,我們這里之所以用gcore而不是jmap來dump,主要是因為在OOM時,通常JVM已經(jīng)無法正常使用jmap來dump了(針對本次排查就是這種情況),如果你一定要使用jmap來操作,那么他會報錯,無法進行堆dump,同時會在錯誤信息中告訴我們可以嘗試使用jmap -F參數(shù)來進行堆dump,但是加上這個參數(shù)后,你會發(fā)現(xiàn)噩夢開始了,因為此時雖然能正常進行dump,但是速度可以說是慘不忍睹,4G的堆dump時間要按小時算,本質(zhì)上是因為當我們使用jmap -F來進行堆dump的時候?qū)嶋H上底層使用了ptrace來dump(使用ptrace讀取目標進程內(nèi)存然后寫出到文件),由于ptrace一次最多只能讀取4字節(jié)(32位機器),所以導(dǎo)致他的速度也極其的慢;而gcore生成速度相對于正常jmap來說也是比較快的,對于jmap -F就更快了;所以,基于以上幾點,我們選擇了使用gcore來進行堆dump;

          當我們使用gcoredump完后,因為最終還是需要使用Java系的工具進行內(nèi)存分析,所以還是要將core dump轉(zhuǎn)換為Java的堆dump,此時我們就可以執(zhí)行以下命令來轉(zhuǎn)換了:

          注意,core dump完成后就可以先重啟服務(wù)了,重啟完服務(wù)再進行下面的步驟;

          # 生成堆dump
          jmap -dump:format=b,file=heap.hprof `which java` core.100

          堆dump生成完畢后,將其下載下來,然后導(dǎo)入eclipse Memory Analyzer(MAT)開始分析,發(fā)現(xiàn)大量org.bouncycastle.jce.provider.BouncyCastleProvider實例被javax.crypto.JceSecurity類的verificationResults這個靜態(tài)字段持有,下面就可以開始源碼分析了;

          具體怎么分析出是這個地方內(nèi)存泄漏這里不做單獨說明了,可以自行查詢官方使用文檔,后續(xù)也會考慮單獨出一期分析方法的文章;

          問題分析

          查看javax.crypto.JceSecurity的源碼,發(fā)現(xiàn)verificationResults是一個map,同時只在getVerificationResult這個方法中被放入了數(shù)據(jù),源碼如下:

          cf0b24f65bc18d4bb6c81830cc3e949b.webp

          經(jīng)過結(jié)合我們的業(yè)務(wù)代碼分析,發(fā)現(xiàn)是我們調(diào)用了javax.crypto.Cipher.getInstance(java.lang.String, java.security.Provider)這個方法,這個方法調(diào)用了javax.crypto.JceSecurity.getVerificationResult方法;getVerificationResult這個方法比較簡單,就是對我們提供的java.security.Provider所在的jar進行簽名校驗,校驗完畢后將我們提供的Provider作為key、校驗結(jié)果作為value放入verificationResults這個map緩存,下次就不用校驗了,但是這里有個問題,就是這個緩存沒有任何清理機制,也就意味著我們?nèi)绻l繁調(diào)用javax.crypto.Cipher.getInstance(java.lang.String, java.security.Provider)來獲取AES實例的話,就是可能會導(dǎo)致內(nèi)存泄漏的,經(jīng)過我們驗證,也確實是這樣,可以使用以下代碼復(fù)現(xiàn):

          注意指定jvm參數(shù): -Xmx128m

          import java.security.NoSuchAlgorithmException;

          import javax.crypto.Cipher;
          import javax.crypto.NoSuchPaddingException;

          import org.bouncycastle.jce.provider.BouncyCastleProvider;

          /**
          * @author JoeKerouac
          * @date 2023-08-24 12:50
          */
          public class Test {

          public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException {
          for (int i = 0; i < 500; i++) {
          Cipher c = Cipher.getInstance("AES", new BouncyCastleProvider());
          }
          }

          }

          問題解決

          既然問題定位到了,那解決起來就比較簡單了,我們可以把Provider實例全局共享,或者使用Provider的名字來獲取AES實例,這樣不去反復(fù)創(chuàng)建Provider而是使用同一個Provider,在JceSecurity中自然也不會內(nèi)存泄漏,代碼如下:

          至于作者為什么不使用單例?那是因為AES并不是線程安全的,無法全局共享,當然,可以使用單例然后自行控制并發(fā),或者使用對象池技術(shù)、ThreadLocal等來解決;

          import java.security.NoSuchAlgorithmException;
          import java.security.NoSuchProviderException;
          import java.security.Security;

          import javax.crypto.Cipher;
          import javax.crypto.NoSuchPaddingException;

          import org.bouncycastle.jce.provider.BouncyCastleProvider;

          /**
          * @author JoeKerouac
          * @date 2023-08-24 12:50
          */
          public class Test {

          private static BouncyCastleProvider provider = new BouncyCastleProvider();

          public static void main(String[] args)
          throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
          // 使用全局共享的provider
          Cipher cipher = plan1();

          // 使用provider的名字獲取AES實例,其實本質(zhì)上也是全局共享了provider
          // 注意,如果要使用provider的名字獲取AES實例,要先注冊
          Security.addProvider(new BouncyCastleProvider());
          cipher = plan2();
          }

          public static Cipher plan1() throws NoSuchAlgorithmException, NoSuchPaddingException {
          return Cipher.getInstance("AES", provider);
          }

          public static Cipher plan2() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException {
          return Cipher.getInstance("AES", "BC");
          }

          }

          問題溯源

          這應(yīng)該是一個比較容易發(fā)現(xiàn)的問題,既然這樣,那有沒有人提出這個問題呢,想到這里,作者打開了Google,經(jīng)過一番搜索后(其實很容易就能搜到,只需要搜索關(guān)鍵字javax.crypto.JceSecurity#getVerificationResult即可),發(fā)現(xiàn)確實有人給jdk提了這個bug,而且也給出了解決方案,代碼已經(jīng)合并到了master,不過截至發(fā)文時,在作者使用的eclipse jdk(Temurin)中,jdk8jdk17這兩個版本的最新發(fā)布中(2023-07-25發(fā)布)仍然存在該問題,并未修復(fù),所以如果遇到該問題,還是需要使用上邊的解決方案來處理;

          官方bug記錄:https://bugs.openjdk.org/browse/JDK-8168469

          至此,我們的問題已經(jīng)解決了,通過本篇文章,你應(yīng)該大概知道了線上發(fā)生OOM時的處理流程了,以后碰到類似問題可以按照本流程來直接套用,至少大多數(shù)場景下都是可以的;

          聯(lián)系我
          • 作者微信:JoeKerouac

          • 微信公眾號(文章會第一時間更新到公眾號,如果搜不出來可能是改名字了,加微信即可=_=|):代碼深度研究院

          • GitHub:https://github.com/JoeKerouac


          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  韩导航激情亚洲丁香幼导航 | 色吧AV乱伦 | 免费黄色色情成人影片 | 翔田千里无码AV在线观看 | 艹逼无码黄色的视频禁止 |