簡單記錄一次遠古版本dubbo發生的PermGen space異常

  環境介紹: dubbo的版本是比較舊的版本,  肯定是小於2.5的, jdk版本是1.7, 默認使用的是HotSpot虛擬機

  前提說明: dubbo版本應該就是最原始的2.x的版本, 由於在這個基礎上公司還經過了自己的自定義封裝, 所以升級的話肯定是沒戲的, 其次, 也是由於某些模塊很少使用到, 所以一直沒暴露出來問題

  生產環境oom現象: 生產上剛啓動一段時間內是可以正常使用的,  幾天之後服務就掛了, 必須重啓之後才能重新對外提供服務, 通過日誌可以發現報錯:OutOfMemoryError  PermGen  space,  這種情況用腳都能猜出來是內存泄露,  也是jvm中永久代內存有些一直沒有被回收, 而且還不斷的往永久代中新增東西

  網上的解決方案: 使用-XX:MaxPermSize調整一下永久代的最大空間!  尼瑪, 這就很離譜, 這不是治標不治本麼, 這種方法頂多就是你的系統一個星期oom變爲了兩三個星期再oom一次了, 如果在oom之前又有新的項目上線重啓一下服務,  都可以苟活一段時間了

  下面就簡單介紹一下我這次出現的問題吧

1. 啥是永久代

  首先要知道,  方法區是jvm規範, 而永久代是方法區的實現, 他們就類似於接口和實現類的關係, 所以下面我把方法區和永久代看作是等價的

  在我們java程序要啓動的時候, 就需要加載很多的類, 可以把每個類看作是class文件,   通過類加載器加載進了永久代, 我們就把永久代中數據看作是類的元數據, 其中包含了常量池, 字段, 方法等信息

  再深入一點, 我們知道java之中還有一個Class對象, 這個Class對象就是根據永久代中的元數據生成的, 這放在java堆中;

  實例化對象的時候有兩種方式:

  方式一: 根據元數據來進行實例化的,  下圖所示, Class對象對於同一個類加載器加載的,  只能有一個,  和方法區中元數據一一對應,而實例可以有多個

  方式二: 使用反射根據Class對象進行實例化對象

  我們常用的獲取Class對象有三種方式: 

  (1)Class.forName("ClassName"):通過類的元數據中的Class對象引用獲得Class對象

  (2)object.getClass():通過實例對象中保存的對類的元數據的引用獲取類的元數據,再通過元數據中對Class對象的引用獲取Class對象

  (3)ClassName.class:通過類的元數據中的Class對象引用獲得class對象

 

 

 

 2. 永久代有大小麼?

  從上面的圖中可以看到永久代其實也是屬於堆中一部分,  可以在啓動的時候設置永久代的容量和最大的容量,  例如: -XX:PermSize=64m,-XX:MaxPermSize=128m

  那麼問題來了, 永久代如果設置太小了怎麼辦? 結果就是java程序啓動的時候,  都會報永久代oom,  或者項目啓動了之後需要動態加載第三方jar包的時候, 發生oom

  永久代進行gc的條件: 

  (1) 該類的實例都被回收 

  (2) 加載該類的classLoader已經被回收

  (3) 該類不能通過反射訪問到其方法,而且該類的java.lang.class沒有被引用 當滿足這3個條件時,是可以回收,但回不回收還得看jvm

  如果你的服務器上oom了, 第一反應不是重啓, 而是最快時間拷貝一份堆棧快照, 可以使用jmap -dump:live, format=b,file=dumpxxx.hprof pid,  其中dump表示要導出一份堆棧信息文件, live表示要把活的對象導出,  format=b, 文件格式是二進制;  file表示要導出的文件保存的全路徑;  pid表示進程id

 

3. OOM具體原因和解決方案

  這裏就不放堆棧信息了, 公司內部的東西,  反正就是MAT工具進行一頓猛分析, 發現裏面那種ClassLoader$ApplicantClassLoader比較多, 推測加載的元數據信息到永久代中很多, 然後其他的信息也看不出來啥, 水平比較菜o(╥﹏╥)o

  然後我嘗試在本地搭建了環境, 試試能不能復現出來, 調整了一下堆棧參數, 使用jmeter壓測了幾個小時之後, 還真的復現了

  因爲以前是沒有出現過的這個問題,  先檢查代碼, 都是業務代碼, 沒有涉及到cglib動態代理這種的使用!

  肯定是這次上線的版本新功能有哪裏涉及到了,  經過排查,  這次新的東西就是多使用到了一個框架層次的工具類, 是對緩存的抽象, 通過生成緩存的代理類, 去操作redis, 猜測就是這個類的影響

  最簡單直接的排查方式就是自己手動寫一個redis的工具類, 然後統統替換掉那個工具類, 然後壓測一段時間, 就沒有這個問題了

  通過手動debug的方式, 最後到了一個Proxy的工具類中,  就是這裏涉及到了cglib動態代理, 不斷的拼接java類字符串, 然後加載到方法區中, 生成class對象, 然後通過class對象反射生成實例, 可能就是這裏的原因, 由於這個類看的不是很懂, 我就去github上的dubbo的issue搜了一下oom,看有沒有相類似的問題,  還真的被我搜出來了, 

 

  繼續點進去發現了一些很有意思的東西

  (1) proxy instance cause a PermSpace OOM #6742  

  因爲 org.apache.dubbo.common.bytecode.Proxy 中使用的Proxy對象緩存導致。PROXY_CACHE_MAP 緩存的Proxy實例,使WeakReference,full GC 後會釋放該Proxy實例再次申請對象實例時,Proxy會重新創建Proxy的Class對象,最終導致PermGen space內存溢出。應該修改爲緩存該Proxy Class,而不是 Proxy 對象實例。

  (2) commit記錄

  最大的改變其實就是增加了這麼一個Map, 用於緩存生成代理類的Class對象,  每次先去PROXY_CACHE_MAP看看實例對象有沒有, 沒有的話, 再去PROXY_CLASS_MAP中找到對應的Class對象, 如果還是沒有才會去拼接java類, 然後加載到永久代中, 然後再緩存Class對象,  以後就不需要再加載了

  而由於我這裏dubbo項目比較老,  每次都要去加載類的信息到永久代中, 時間久了, 永久代就掛了

 

  既然找到了問題所在, 改的話, 也就簡單了, 直接打開dubbo的源碼抄就好了, 畢竟我可是ctrl+c  ctrl+v的高手, 這點我還是蠻有自信的

  這個問題只有在dubbo2.7的版本才被修復,  可以打開2.7.x版本的org.apache.dubbo.common.bytecode.Proxy類的代碼, 抄一抄就解決了

 

4. 總結

   一個oom的問題涉及的東西是真的多,  首先涉及到要很瞭解java類的加載機制, 以及jvm內存結構,  cglib動態代理, 在服務器端使用jmap導出堆棧信息,  MAT內存分析工具的使用,  jmeter性能壓測,  dubbo源碼閱讀以及調試,  github查找相關問題,  真尼瑪的麻煩o(╥﹏╥)o

  而且這次真的是體會到了一活躍的開源社區的強大之處, 要是一個使用的人都比較少的框架,  都沒幾個人, 靠自己很難發現問題的所在之處, 並不是每個人都有修改源碼的能力的, 害

  再一次提醒我們要多提高技術, 有時間就關注開源社區的一些問題以及最新動向

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章