一次gc異常排查

近期公司公網(wǎng)接口被頻發(fā)攻擊刷垃圾數(shù)據(jù),某些不常用接口一晚上被刷了幾十萬次,此背景下接口項目頻繁出現(xiàn)OOM的情況搀别,主要表現(xiàn)如下圖:


gc異常.png

如圖所示怕享,每次fgc都無法回收內(nèi)存执赡,很明顯項目代碼中有內(nèi)存泄漏的情況存在,只能重啟項目臨時救急函筋。隨后看近期代碼變動記錄也未發(fā)現(xiàn)明顯問題沙合,只得讓運維協(xié)助導出內(nèi)存dump來分析具體原因了。

分析問題前先明確下內(nèi)存異常的概念:
內(nèi)存溢出 out of memory跌帐,是指程序在申請內(nèi)存時首懈,沒有足夠的內(nèi)存空間供其使用,出現(xiàn)out of memory谨敛;
內(nèi)存泄露 memory leak究履,是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間脸狸,一次內(nèi)存泄露危害可以忽略最仑,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光炊甲。
memory leak最終會導致out of memory盯仪!

第一次排查

第一次導出使用的命令是jmap -dump:format=b,file=heap.bin <pid>。沒有選在內(nèi)存占用最高的時候?qū)С雒鄞校晕募挥?G多大小全景,這不會影響實例占用內(nèi)存的比例情況因為對象占用是逐漸遞增成比例的。不過這個命令有個問題就是導出的內(nèi)存不是fullgc以后的內(nèi)存情況牵囤,因為內(nèi)存溢出的出現(xiàn)是fullgc后無法回收的那些對象有問題爸黄,當時心知這個命令會有問題但是當時文件已經(jīng)導出不想太麻煩運維再導一次也就沒有太在意滞伟,事實證明嫌麻煩的后果就是事情會變得更麻煩了。

  1. 拿到dump文件后使用jdk中自帶的jvisualvm進行分析炕贵,單看內(nèi)存占用情況如下:


    內(nèi)存類信息.png
  2. 內(nèi)存中hashMap對象實例數(shù)最多且占用內(nèi)存也是最多的梆奈,我們雙擊map對象查看詳情,一般這種實例數(shù)較多的我們很容易就能找到問題對象称开,如下圖所示亩钟,通過查看多個對象實例發(fā)現(xiàn)key是X509CertImpl的Map對象有很多,我們點擊查看垃圾回收根結點查看


    最近的垃圾回收節(jié)點.png
  3. 查看根結點后會把對象調(diào)用路徑完整的展示出來鳖轰,我們把根路徑復制出來看看具體是哪些調(diào)用清酥。如下圖點擊復制從根開始的路徑


    復制從根開始的路徑.png
  1. 最后得到如下的一個完整路徑,發(fā)現(xiàn)是阿里云 oss的引用蕴侣,初步發(fā)現(xiàn)問題后就結合項目代碼一起比對檢查焰轻。
this     - value: java.util.HashMap$Node #335002
 <- [22]     - class: java.util.HashMap$Node[], value: java.util.HashMap$Node #335002
  <- table     - class: java.util.HashMap, value: java.util.HashMap$Node[] #28817
   <- map     - class: java.util.HashSet, value: java.util.HashMap #32007
    <- trustedCerts     - class: sun.security.ssl.X509TrustManagerImpl, value: java.util.HashSet #12745
     <- trustManager     - class: org.apache.http.ssl.SSLContextBuilder$TrustManagerDelegate, value: sun.security.ssl.X509TrustManagerImpl #2346
      <- tm     - class: sun.security.ssl.AbstractTrustManagerWrapper, value: org.apache.http.ssl.SSLContextBuilder$TrustManagerDelegate #2357
       <- trustManager     - class: sun.security.ssl.SSLContextImpl$TLSContext, value: sun.security.ssl.AbstractTrustManagerWrapper #2358
        <- context     - class: sun.security.ssl.SSLSocketFactoryImpl, value: sun.security.ssl.SSLContextImpl$TLSContext #2343
         <- socketfactory     - class: org.apache.http.conn.ssl.SSLConnectionSocketFactory, value: sun.security.ssl.SSLSocketFactoryImpl #2363
          <- val     - class: java.util.concurrent.ConcurrentHashMap$Node, value: org.apache.http.conn.ssl.SSLConnectionSocketFactory #2345
           <- [11]     - class: java.util.concurrent.ConcurrentHashMap$Node[], value: java.util.concurrent.ConcurrentHashMap$Node #162146
            <- table     - class: java.util.concurrent.ConcurrentHashMap, value: java.util.concurrent.ConcurrentHashMap$Node[] #13097
             <- map     - class: org.apache.http.config.Registry, value: java.util.concurrent.ConcurrentHashMap #22642
              <- socketFactoryRegistry     - class: org.apache.http.impl.conn.DefaultHttpClientConnectionOperator, value: org.apache.http.config.Registry #7070
               <- connectionOperator     - class: org.apache.http.impl.conn.PoolingHttpClientConnectionManager, value: org.apache.http.impl.conn.DefaultHttpClientConnectionOperator #2345
                <- [2073]     - class: java.lang.Object[], value: org.apache.http.impl.conn.PoolingHttpClientConnectionManager #2342
                 <- elementData     - class: java.util.ArrayList, value: java.lang.Object[] #156896
                  <- connectionManagers     - class: com.aliyun.oss.common.comm.IdleConnectionReaper, value: java.util.ArrayList #74889
                   <- <class> (thread object)     - class: com.aliyun.oss.common.comm.IdleConnectionReaper, value: com.aliyun.oss.common.comm.IdleConnectionReaper class IdleConnectionReaper
  1. 排查代碼發(fā)現(xiàn)項目中上傳頭像的功能使用到了oss,代碼如下
    public static boolean fileUploadToOSS(InputStream uploadFilePath, String objectKey){
        URL url = null;
        // 使用默認的OSS服務器地址創(chuàng)建OSSClient對象,不叫OSS_ENDPOINT代表使用杭州節(jié)點昆雀,青島節(jié)點要加上不然包異常
        OSSClient client = new OSSClient(OSSConfigure.getInstance().getOssEndPoint()
                , OSSConfigure.getInstance().getAccessId(), OSSConfigure.getInstance().getAccessKey());
        try {
            ObjectMetadata objectMeta = new ObjectMetadata();
            objectMeta.setCacheControl("no-cache");
            objectMeta.setHeader("Pragma", "no-cache");
            objectMeta.setContentEncoding("utf-8");
            objectMeta.setContentType("image/png");
            client.putObject(bucketName, Objectkey, file, objectMeta);
            return true;
        } catch (FileNotFoundException e) {
            log.error("文件圖片OSS上傳失斎柚尽!", e);
        }
        return false;
    }
  1. 代碼中發(fā)現(xiàn)每次上傳頭像都會new一個新的OssClient狞膘,而且圖片上傳完之后還沒有手動關閉鏈接揩懒,并且圖片流也沒有進行關閉,另一個關鍵點是導出這個dump那段時間上傳頭像的接口正在被刷挽封,OssClient的對象創(chuàng)建了很多并且沒有關閉旭从,種種跡象告訴我這肯定是問題的根源。
    正確的使用方式參考:https://bbs.aliyun.com/simple/t257085.html场仲,
    其他人也有同樣的問題:https://blog.csdn.net/ashur619/article/details/82835662和悦,這個情況和我們極相似。

最后修改完代碼并上線渠缕,自此我天真的以為問題已經(jīng)得到了徹底解決鸽素,但是沒有過幾天同樣的問題又出現(xiàn)了,當時感覺滿腦子問號??
?.png

沒辦法問題來了就接著找吧 !

第二次排查

  1. 因為之前使用的jmap命令有問題這次加個histo:live參數(shù)亦鳞,完整命令jmap -dump:format=b,live,file=heap.bin <pid>,這個命令會執(zhí)行一次fgc馍忽,并導出gc后的內(nèi)存情況,并且下載了mat來更清晰的解析dump文件燕差。首次打開文件mat會生成自己的泄漏建議報告遭笋,我們可以打開看一下它提供的分析報告,選Leak Suspects Report點finish即可徒探,分析后生成圖表和問題報告瓦呼,problem a幾乎把1.6的內(nèi)存快占滿了,并且也準確指出了問題出現(xiàn)在哪
    The memory is accumulated in one instance of com.alibaba.fastjson.util.IdentityHashMap$Entry[]

    report.png

  2. 點擊details進去后查看具體情況测暗,我們發(fā)現(xiàn)對象ParseConfig占用了93%的內(nèi)存空間央串,而ParseConfig下引用了N多個fastjson自定義的IdentityHashMap


    image.png
  3. 我們再看下內(nèi)存空間中整體的對象實例情況磨澡,到overview頁面點histogram查看實例直方圖如下:


    image.png
  4. ParserConfig只有一個實例 ,我們結合fastjson源碼看下這個ParserConfig和IdentityHashMap的具體關系


    image.png
  5. ParseConfig是一個單例质和,IdentityHashMap是用來存放json對象反序列化器的稳摄,并且key是一個反射類Type類型,我們再看下為什么會存放這么多的反序列化器呢饲宿,因為原則上一種類型的對象(一種Type對象)只需要一個反序列化器就可以了厦酬。下面我們看下這個deserializers里都存了哪些對象,點擊ParserConfig對象java Basics Open In Dominator Tree,查看依賴樹


    image.png
  6. 打開后如下,分析發(fā)現(xiàn)IdentityHashMap的key是一個gson的對象瘫想,而需要反序列化的類是我們項目中自定義的仗阅,我又打開了其他的IdentityHashMap發(fā)現(xiàn)里面的結構和反序列化對象都是一樣的,同對象為什么會生成如此多個反序列化器呢殿托,Map存了這么多對象說明map的key都是不一樣的霹菊,因為map的key是不能重復的剧蚣,IdentityHashMap同樣也是支竹,再查看了這些IdentityHashMap的key發(fā)現(xiàn)每個key的類型都是ParameterizedTypeImpl,但是內(nèi)存地址值都是不一樣的鸠按,說明key每次都是new的一個新ParameterizedTypeImpl對象礼搁。


    image.png
  7. 再看了下IdentityHashMap的put方法,key是直接拿對象的hash值做運算并存放到buckets指定坐標上的目尖,如此我們可以確定這跟key的生成是有關系的馒吴,我們結合上面查到的RespVipBaseDTO再看下key對象到底是怎么生成的,代碼中JSON.parseObject()的時候會到ParserConfig中按key尋找對應的反序列化器瑟曲,沒有的話就會新創(chuàng)建一個新的饮戳。

RespBaseDTO<RecommendContentDTO> result = 
JSON.parseObject(resultString, new TypeToken<RespVipBaseDTO<RecommendContentDTO>>() {}.getType());
  1. 如上代碼會用new TypeToken(){}.getType()當作key去查找合適的反序列化器,那我們看下這個key是怎么生成的。這里只貼出創(chuàng)建時的代碼洞拨,如下代碼扯罐,我們發(fā)現(xiàn)ParameterizedTypeImpl都是通過new創(chuàng)建的,說明每次使用TypeToken時都會創(chuàng)建一個新的烦衣,這也是為什么IdentityHashMap越來越多的根本原因歹河。
if (type instanceof ParameterizedType) {
      ParameterizedType p = (ParameterizedType) type;
      return new ParameterizedTypeImpl(p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
}
  1. 其實原因還是開發(fā)人員在使用json時串用了不同廠商的json工具,TypeToken是谷歌gson的類花吟,使用的JSONObject卻是阿里fastjson的秸歧,正確的使用方法應該是使用TypeReference,TypeReference.getType()時同樣會返回一個Type類型衅澈,但是這個Type類型是通過java反射獲取到的類的基本信息键菱,以此Type作為key是不會有這樣的問題的:
BaseReqDTO<CommonReqDTO> baseReqDTO = parseRequest(request, new TypeReference<BaseReqDTO<CommonReqDTO>>() {}.getType());
  1. 可以簡單寫一些驗證代碼做個簡單的驗證,運行后發(fā)現(xiàn)使用TypeToken創(chuàng)建Type會造成程序運行緩慢并最終造成OOM異常今布,TypeToken換成TypeReference后沒有此問題出現(xiàn):
        for(int i = 0;i < 1000000; i++){
            JSON.parseObject("\"userId\":\"213\"",
                    new TypeToken<RespVipBaseDTO<OqsChoicenessProducts>>() {
                    }.getType());
            MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
            MemoryUsage usage = memorymbean.getHeapMemoryUsage();
            if(i % 1000 == 0){
                System.out.println("第:" + i + "次");
                System.out.println("INIT HEAP: " + usage.getInit());
                System.out.println("MAX HEAP: " + usage.getMax());
                System.out.println("USE HEAP: " + usage.getUsed());
            }
            if(i% 50000 == 0){
                System.gc();
                System.out.println("第:" + i + "次");
                System.out.println("INIT HEAP: " + usage.getInit());
                System.out.println("MAX HEAP: " + usage.getMax());
                System.out.println("USE HEAP: " + usage.getUsed());
            }
        }

總結:

  1. 發(fā)現(xiàn)問題可以先看代碼最近是否有大的變動纱耻,有沒有使用不合理的地方芭梯,結合系統(tǒng)最近情況作出判斷,沒發(fā)現(xiàn)明顯問題就果斷導dump文件開始做分析弄喘。
  2. 這次排查中看代碼的時間要比看dump分析的時間多得多玖喘,要單從分析工具中找到具體問題是不可能的,或者是因為我還沒掌握到更深的使用方法蘑志。
  3. 代碼質(zhì)量問題亟待解決
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末累奈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子急但,更是在濱河造成了極大的恐慌澎媒,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件波桩,死亡現(xiàn)場離奇詭異戒努,居然都是意外死亡,警方通過查閱死者的電腦和手機镐躲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門储玫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人萤皂,你說我怎么就攤上這事撒穷。” “怎么了裆熙?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵端礼,是天一觀的道長。 經(jīng)常有香客問我入录,道長蛤奥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任僚稿,我火速辦了婚禮凡桥,結果婚禮上,老公的妹妹穿的比我還像新娘贫奠。我一直安慰自己唬血,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布唤崭。 她就那樣靜靜地躺著拷恨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谢肾。 梳的紋絲不亂的頭發(fā)上腕侄,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音,去河邊找鬼冕杠。 笑死微姊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的分预。 我是一名探鬼主播兢交,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼笼痹!你這毒婦竟也來了配喳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤凳干,失蹤者是張志新(化名)和其女友劉穎晴裹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體救赐,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡涧团,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了经磅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泌绣。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖馋贤,靈堂內(nèi)的尸體忽然破棺而出赞别,到底是詐尸還是另有隱情畏陕,我是刑警寧澤配乓,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站惠毁,受9級特大地震影響犹芹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鞠绰,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一腰埂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蜈膨,春花似錦屿笼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至灶壶,卻和暖如春肝断,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工胸懈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留担扑,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓趣钱,卻偏偏與公主長得像涌献,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子首有,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348