最近筆者有點忙,這次OOM事故發生過去兩週前,記得筆者那天正帶著家人在外地玩,正中午跟友人吃飯的時候,釘釘連續告警爆表,接著就是釘釘電話(顯示廣東抬頭)一看就知道BBQ了,又一次故障發生了,今天把那次故障覆盤一下,做個總結,也給小夥伴分享一下 我是怎麼從接到告警開始,怎麼一步一步分析故障,然後定位到問題,最後完美解決,成功上線解決問題的。
上述告警內容,由於筆者所在服務是用CMS垃圾回收器,當其GC次數太頻繁,達到公司監控平臺設定的閾值時,就會通過釘釘通知告知開發者,傳送到對應的控制檯上。這個異常先從字面意義上來說倒也比較明顯,如果老年代裡的物件太多,無法提供空間容納年輕代傳遞過來的物件的時候,就會觸發FULL GC。
這裡我們先簡單分析一下,物件什麼情況下會進入老年代,以及老年代又是在什麼情況下會觸發FULL GC?只有先知道了原理性東西,你才能帶著思路去分析,真實線上場景屬於對應哪種情況
首先科普一下物件什麼情況下會進入老年代?
1)躲過15次GC之後進入老年代
線上由於比較麻煩dump執行緒。而且現場已經過去了,所以我還是自己寫了一段壓測程式碼(類似Jmeter效果),來壓測相應的總入口,看看具體是哪個物件佔了大記憶體
很明顯是有一個nashorn相關物件佔據了比較大的佔比。那這個物件其實對應筆者的程式是
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Compilable compEngine = (Compilable) engine;
try {
CompiledScript compile = compEngine.compile(script);
}catch(Exception e){
}
簡單來說,Nashorn的編譯入口可以從 Context.compileScript()
開始看:[ JavaScript原始碼 ] -> ( 語法分析器 Parser ) -> [ 抽象語法樹(AST) ir ] -> ( 編譯優化 Compiler ) -> [ 優化後的AST + Java Class檔案(包含Java位元組碼) ] -> JVM載入和執行生成的位元組碼 -> [ 執行結果 ]
此過程是十分耗時的,每次執行eval
去執行js ,都需要編譯成位元組碼、然後載入執行。同時會將編譯過的位元組碼快取起來,以便後續使用,因此載入的類會長時間存活,佔用很大的記憶體空間。
所以筆者嘗試將CompiledScript這一物件第一次編譯完後,本地快取起來用
private static Map<Long, CompiledScript> scriptMap = new ConcurrentHashMap<>();
快取起來,下一次如果已經存在,就直接拿來用。
重新壓測後效果還是明顯的
總結
線上場景 特別對於一些新的框架或技術 如果你的流量很大,筆者那時參與了這個專案,工期特別短,功能又特別多,想著先上線,下一步再做壓測,想不到等不到下一步問題就暴露出來了?