近期上線了一個系統,鑑權部分使用了Groovy腳本,示例代碼如下
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
String function = String.format("def getTargetParamValue(%s) {return \"%s\"}", "o", "$o");
engine.eval(function);
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("getTargetParamValue", "test-string");
System.out.println(result);
這段代碼定義了一個Groovy的方法,根據傳進去的參數返回對應的值。
由於生產環境流量很大,這段代碼被頻繁執行。測試時的代碼如下
public class ScriptEngineTest {
public static void main(String[] args) {
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
//測試時改爲死循環
for (int i = 0;; i++) {
try {
String function = String.format("def getTargetParamValue(%s) {return \"%s\"}", "o", "$o");
engine.eval(function);
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("getTargetParamValue", "test-string");
System.out.println(result);
TimeUnit.MICROSECONDS.sleep(100);
System.out.println(new Date().toLocaleString());
} catch (Exception e) {
String errorMsg = String.format("異常!%s", e.getMessage());
System.out.println(errorMsg);
}
}
}
}
模擬生產環境的情況,每秒鐘執行10次。通過VusualVM觀察JVM
-
CPU使用情況,可以看到在每次堆內存擴容的時候,CPU使用量會有明顯增加
-
堆內存使用情況
-
metaspace使用量一直在增加
-
類加載情況,total loaded classes一直在增加
-
線程
- dump內存
可見,每次循環中生成的 Groovy method在方法執行完成之後並沒有被釋放掉,導致metaspace的使用量一直增加,最終撐爆JVM
針對以上問題,解決方法爲每次將生成的方法緩存下了,下次要執行的時候從緩存中取。
private final ConcurrentHashMap<String, Invocable> concurrentHashMap = new ConcurrentHashMap<>();
private Object getInvokeResult(Object targetParam, String paramName, String expression) throws Exception {
//targetParamClassName="com.umgsai.web.home.vo.NodeVO"
String targetParamClassName = targetParam.getClass().getName();
//expression="$nodeVO.bizOwner"
//paramName="nodeVO"
String functionKey = String.format("%s_%s_%s", targetParamClassName, paramName, expression);
functionKey = StringUtil.replaceChars(functionKey, "$", "");
functionKey = StringUtil.replaceChars(functionKey, ".", "_");
//functionKey爲方法的名稱和concurrentHashMap的key,這裏需要去掉特殊字符
Invocable invocable = concurrentHashMap.get(functionKey);
if (invocable != null) {
//如果緩存中有,直接調用
return invocable.invokeFunction(functionKey, targetParam);
}
//如果緩存中沒有,生成方法,並且存到concurrentHashMap
synchronized (lock) {
invocable = concurrentHashMap.get(functionKey);
if (invocable == null) {
String function = String.format("def %s(%s) {return \"%s\"}", functionKey, paramName, expression);
engine.eval(function);
invocable = (Invocable) engine;
concurrentHashMap.put(functionKey, invocable);
if (log.isInfoEnabled()) {
String msg = String.format("Create new Groovy function, functionKey=%s, paramName=%s, expression=%s",
functionKey,
paramName, expression);
log.info(msg);
}
}
}
if (log.isInfoEnabled()) {
log.info(String.format("Groovy function concurrentHashMap.size=%d", concurrentHashMap.size()));
}
return invocable.invokeFunction(functionKey, targetParam);
}
參考 http://www.zgxue.com/120/1204001.html
https://www.cnblogs.com/fourspirit/p/4332154.html
https://www.jianshu.com/p/b1a46cc02377