天天看點

問題排查之JVM記憶體溢出問題描述:原因分析:解決方案:

問題描述:

一天我正在專心緻志的搬着磚,突然測試同僚找我說伺服器好像崩了,現在所有業務服務都調用不了直接就報錯啦,趕緊看看啥問題。

伺服器崩了這還了得,趕緊先等上管理中心瞄一眼,發現原本部署好的服務狀态全部變為異常了。登上伺服器背景用jmap查了下JVM各個代的記憶體使用率,發現老年代記憶體使用率已經到99.9%了,用jstat看也是發現一直JVM一直在做Full GC。毫無疑問這是堆記憶體溢出了。

原因分析:

問了下之前都有什麼操作,測試也隻是說就把服務都啟動完之後調了幾個接口就這樣了。因為看現象是記憶體溢出的問題,為了快速複現,是以就把應用給重新開機了,但是給JVM配置設定記憶體調小了一半,并且部署的服務也減少到一半,然後再給JVM加上

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/opt/dump

用來在JVM記憶體溢出時自動在指定的目錄生成堆轉儲快照,然後交給同僚用慢慢等着複現了。

過了一陣子,果然發現又記憶體溢出了,拿着新鮮出爐的堆轉儲快照檔案分析看看究竟是什麼原因。用MAT打開堆轉儲檔案之後,等它解析完之後打開的第一個頁面是:

問題排查之JVM記憶體溢出問題描述:原因分析:解決方案:

這個界面會顯示不同對象的記憶體占比,感覺沒什麼好看的,點選Leak Suspects發現最占記憶體比的對象是org.apache.ibatis.session.Configuration的執行個體,已經占了1.8G記憶體了。

點選左上方的Open Dominator Tree for entire heap,可檢視每個對象占用記憶體大小,如下圖:

問題排查之JVM記憶體溢出問題描述:原因分析:解決方案:

從圖中可見除了最上面兩個,最占記憶體大小的就是下面連續的Configuration。

這裡先用OQL語言查找出所有的Configuration執行個體,顯示有75個,然後任一層一層的點下去看下裡面都是什麼占用了記憶體,可以看到主要是Configuration執行個體的兩個屬性:sqlFragments以及mappedStatements。

它們的類型都是StricMap,不熟悉的話大家可以去翻下mybatis中Configuration實作的源碼,它有個内部類就是StrictMap,繼承自HashMap是一個映射字典來着。

在Configuration中的sqlFragments作用主要是用來緩存mapper檔案中sql節點,key為namespace+sql節點的id,value為sql節點内容轉化成的XNode對象;而mappedStatements作用則是用來緩存mapper檔案中的statement,即各種select、insert、update、delete節點。key也是namespace+各個節點的id,value為節點内容轉化成的mappedStatement。

随意打開了幾個Configuration執行個體檢視裡面sqlFragments和mappedStatements都緩存了哪些mapper的内容,居然發現這幾個Configuration執行個體裡面緩存的mapper檔案大概都差不多,不信邪的再多點幾個執行個體觀察裡面的mapper,随意瞄了幾眼仍然能看到重複的。

也就是說着75個Configuration執行個體幾乎都是把所有mapper檔案都緩存了一道。

這裡先說下我們應用結構,由衆多獨立且功能不同微服務組成(當然也按照一定規則來可以互相調用)。每個微服務都會給它配置一個spring應用上下文來管理bean加載、維護bean直接依賴、負責bean什麼周期等等。

然後需要對資料庫進行操作的微服務當然也要給對應的spring上下文配置好資料源什麼的。其中有個org.mybatis.spring.SqlSessionFactoryBean(用來構造SqlSessionFactory的)有個屬性mapperLocations是用來配置臊面mapper檔案位置的。

一般來說功能不同的微服務基本上用到的資料庫表也有不同,是以對應用到的資料庫表映射的mapper檔案也不同。就好比服務A如果就是更新A商品資訊,那麼隻查A商品對應的資料庫表A,也隻緩存表A對應的mapper檔案而不用去管B。也就是說正常情況下具體微服務需要用到哪些mapper就指定哪些mapper檔案加載就好了。

但是現在翻了大部分spring應用上下文的配置檔案發現都是用 * 來把所有的mapper檔案包括進去了。本來單個微服務隻需要用到幾個mapper檔案,因為偷懶不想一個個mapper檔案指定或者一個個mapper檔案目錄指定而采用 * 來標明全部的mapper檔案(起碼有上百個了)。

這樣不但浪費了大量的記憶體,就連服務啟動速度也給拖慢了。

解決方案:

其實說起來這種全加載的,在自己負責的服務裡面照樣有這個寫法。講道理是不應該有的,隻是以前自己剛上手的時候看别人怎麼做自己也跟着怎麼做了,并沒有考慮其中是否有什麼不妥。至于為什麼一開始别人寫的時候為什麼會采用*來全加載而不是按需加載呢?這個就不得而知了。隻是以後寫代碼看來還是要多多考慮一下,不能全用ctrl c+ctrl v呀。

最後有一個疑惑,感覺即使改成按需加載也隻是在一定程度上緩解了記憶體緩存mapper的消耗。随着後面服務的不斷增加,在緩存mapper上的記憶體消耗也是會越來越大的,而且有些服務要用到的資料庫表肯定是有重複的,要緩存的mapper也會重複。

那麼是否可以把spring應用上下文配置的SqlSessionFactoryBean抽出來公用呢?隻需要標明所有mapper檔案加載一次,後面其他服務都可以用這個SqlSessionFactoryBean而不用每個服務都需要維持自己的一份?