硬件環境:html
廣州華爲服務器 RH2288V3 java
32G 內存,4核,1T硬盤web
軟件環境:apache
JDK1.7 weblogic12 Centos6.5服務器
事情通過:app
2017年10月30號晚上有個同事忽然打電話說新版本發佈後系統常常運行幾天就平白無故崩潰,讓我這邊幫忙定位下是否是程序問題。通常這種程序崩潰基本上都是內存泄露,但檢查最近更新的代碼並無發現有什麼問題,保險起見趕忙上服務器down 一個dump文件下來觀察一下。ui
拿到最新的dump文件以後用MAT分析,一看果真發現內存居高不下(內存佔用達8G)。仔細觀察問題的根源居然是jstl!由於公司的項目運行多年,用的技術五花八門其中有很多頁面用到JSTL..如下就是Memory Analyzer的截圖:this
能夠看到ELEvaluator這個類有一個Map對象,裏面的對象佔用內存達到67108880b(67M)該對象,其直接或間接引用的內存達7152269600b(7G)因此基本能夠判定致使內存泄露的元兇就是它了。而後查看該對象的源碼發現裏面果真有2個靜態Map對象lua
並且很神奇的是這2個map對象都只有put和get可是沒有刪除的方法,也就是說這2個對象只會遞增!同時由於這2個對象非public也就是說其它類也沒法引用...這隻能說是JSTL的一個BUG。而後仔細觀察這2個對象使用的地方:spa
發現只要獲取不到就會建立一個新的對象,也就是說這2個對象都可以刪除。因而解決的辦法來了:直接把sCachedExpressionStrings和sCachedExpectedTypes刪掉。而後從新生成class並替換對應jar包的class。更新上去運行1個星期以後發現內存基本穩定在3G左右(高峯6G低峯1G)。
後來網上查找一下發現老外早就發現這個問題:
http://www.archivum.info/issues@commons.apache.org/2007-09/00118/(jira)-Commented-(EL-1)-(el)-Memory-Leak-EL-Cache-entries-never-removed.html
只是一直沒人給出解決的方法,網上也有人說這個BUG是JSTL1.2的問題,以後的版本已經解決。可是由於項目已經交付,若是臨時替換JAR包難保不會引起其它問題因此就先這樣解決,之後有時間把JSTL從項目中移除出去。
*由於公司用的是weblogic,weblogic自身有不少jar包其中weblogic.server.merged.jar 把JSTL1.2包含進來,若是是使用weblogic作爲服務器則要當心須要修改weblogic.server.merged.jar這裏面的JSTL,否則改了也不會生效。由於按java類加載順序,服務器的jar包加載順序優先於項目的jar包因此若是用的是weblogic的同窗則要當心這個坑。
最後附上ELEvaluator.java的源碼,有興趣的同窗能夠研究下有沒有更好的解決方案,有問題能夠email我。個人郵箱地址是:hurrican_ok@126.com。由於是用jdgui.exe反編譯的你們將就看下
package org.apache.taglibs.standard.lang.jstl; import java.io.Reader; import java.io.StringReader; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.taglibs.standard.lang.jstl.parser.ELParser; import org.apache.taglibs.standard.lang.jstl.parser.ParseException; import org.apache.taglibs.standard.lang.jstl.parser.Token; import org.apache.taglibs.standard.lang.jstl.parser.TokenMgrError; public class ELEvaluator { static Map sCachedExpressionStrings = Collections.synchronizedMap(new HashMap()); static Map sCachedExpectedTypes = new HashMap(); static Logger sLogger = new Logger(System.out); VariableResolver mResolver; boolean mBypassCache; public ELEvaluator(VariableResolver pResolver) { this.mResolver = pResolver; } public ELEvaluator(VariableResolver pResolver, boolean pBypassCache) { this.mResolver = pResolver; this.mBypassCache = pBypassCache; } public Object evaluate(String pExpressionString, Object pContext, Class pExpectedType, Map functions, String defaultPrefix) throws ELException { return evaluate(pExpressionString, pContext, pExpectedType, functions, defaultPrefix, sLogger); } Object evaluate(String pExpressionString, Object pContext, Class pExpectedType, Map functions, String defaultPrefix, Logger pLogger) throws ELException { if (pExpressionString == null) { throw new ELException(Constants.NULL_EXPRESSION_STRING); } Object parsedValue = parseExpressionString(pExpressionString); if ((parsedValue instanceof String)) { String strValue = (String)parsedValue; return convertStaticValueToExpectedType(strValue, pExpectedType, pLogger); } if ((parsedValue instanceof Expression)) { Object value = ((Expression)parsedValue).evaluate(pContext, this.mResolver, functions, defaultPrefix, pLogger); return convertToExpectedType(value, pExpectedType, pLogger); } if ((parsedValue instanceof ExpressionString)) { String strValue = ((ExpressionString)parsedValue).evaluate(pContext, this.mResolver, functions, defaultPrefix, pLogger); return convertToExpectedType(strValue, pExpectedType, pLogger); } return null; } public Object parseExpressionString(String pExpressionString) throws ELException { if (pExpressionString.length() == 0) { return ""; } Object ret = this.mBypassCache ? null : sCachedExpressionStrings.get(pExpressionString); if (ret == null) { Reader r = new StringReader(pExpressionString); ELParser parser = new ELParser(r); try { ret = parser.ExpressionString(); sCachedExpressionStrings.put(pExpressionString, ret); } catch (ParseException exc) { throw new ELException(formatParseException(pExpressionString, exc)); } catch (TokenMgrError exc) { throw new ELException(exc.getMessage()); } } return ret; } Object convertToExpectedType(Object pValue, Class pExpectedType, Logger pLogger) throws ELException { return Coercions.coerce(pValue, pExpectedType, pLogger); } Object convertStaticValueToExpectedType(String pValue, Class pExpectedType, Logger pLogger) throws ELException { if ((pExpectedType == String.class) || (pExpectedType == Object.class)) { return pValue; } Map valueByString = getOrCreateExpectedTypeMap(pExpectedType); if ((!this.mBypassCache) && (valueByString.containsKey(pValue))) { return valueByString.get(pValue); } Object ret = Coercions.coerce(pValue, pExpectedType, pLogger); valueByString.put(pValue, ret); return ret; } static Map getOrCreateExpectedTypeMap(Class pExpectedType) { synchronized (sCachedExpectedTypes) { Map ret = (Map)sCachedExpectedTypes.get(pExpectedType); if (ret == null) { ret = Collections.synchronizedMap(new HashMap()); sCachedExpectedTypes.put(pExpectedType, ret); } return ret; } } static String formatParseException(String pExpressionString, ParseException pExc) { StringBuffer expectedBuf = new StringBuffer(); int maxSize = 0; boolean printedOne = false; if (pExc.expectedTokenSequences == null) { return pExc.toString(); } for (int i = 0; i < pExc.expectedTokenSequences.length; i++) { if (maxSize < pExc.expectedTokenSequences[i].length) { maxSize = pExc.expectedTokenSequences[i].length; } for (int j = 0; j < pExc.expectedTokenSequences[i].length; j++) { if (printedOne) { expectedBuf.append(", "); } expectedBuf.append(pExc.tokenImage[pExc.expectedTokenSequences[i][j]]); printedOne = true; } } String expected = expectedBuf.toString(); StringBuffer encounteredBuf = new StringBuffer(); Token tok = pExc.currentToken.next; for (int i = 0; i < maxSize; i++) { if (i != 0) { encounteredBuf.append(" "); } if (tok.kind == 0) { encounteredBuf.append(pExc.tokenImage[0]); break; } encounteredBuf.append(addEscapes(tok.image)); tok = tok.next; } String encountered = encounteredBuf.toString(); return MessageFormat.format(Constants.PARSE_EXCEPTION, new Object[] { expected, encountered }); } static String addEscapes(String str) { StringBuffer retval = new StringBuffer(); for (int i = 0; i < str.length(); i++) { switch (str.charAt(i)) { case '\000': break; case '\b': retval.append("\\b"); break; case '\t': retval.append("\\t"); break; case '\n': retval.append("\\n"); break; case '\f': retval.append("\\f"); break; case '\r': retval.append("\\r"); break; case '\001': case '\002': case '\003': case '\004': case '\005': case '\006': case '\007': case '\013': default: char ch; if (((ch = str.charAt(i)) < ' ') || (ch > '~')) { String s = "0000" + Integer.toString(ch, 16); retval.append("\\u" + s.substring(s.length() - 4, s.length())); } else { retval.append(ch); } break; } } return retval.toString(); } public String parseAndRender(String pExpressionString) throws ELException { Object val = parseExpressionString(pExpressionString); if ((val instanceof String)) { return (String)val; } if ((val instanceof Expression)) { return "${" + ((Expression)val).getExpressionString() + "}"; } if ((val instanceof ExpressionString)) { return ((ExpressionString)val).getExpressionString(); } return ""; } }