需求背景
前段时间因为在项目里用到了很多的动态值计算,由于计算项很多,计算的公式也会随着需求发生变化,于是想到了动态计算,使用 GroovyShell 脚本语言来对公式进行动态赋值,在进行运算,有很多帖子都是在讲使用 GroovyShell 会导致内存溢出,但是怎么解决这个问题貌似一直没找到,那么下面的代码可以直接拷来用了。
解决方案
GroovyShell 动态计算 ,如果并发量很低 ,只作为个别计算即可, 并发量高往下看。
// java 占位符字符串
%s >= %s + %s
// 程序中使用 String.format 即可自动拼接 format方法可以接受数组
Object[] valueArr = new Object[checkNameArr.length];
int i = 0;
for (String checkNameitem : checkNameArr) {
if (checkFormula.contains("\"")) {
valueArr[i] = "\"" + reflectHelper.getMethodValue(checkNameitem) + "\"";
} else {
valueArr[i] = reflectHelper.getMethodValue(checkNameitem);
}
i = i + 1;
}
String resultFormula = String.format(checkFormula, valueArr);
GroovyShell SHELL = new GroovyShell();
Object calculationResults = SHELL.evaluate(resultFormula);
// SHELL.evaluate 结果值是不确定型的 为了计算的正确性先判断是不是 boolean
if (calculationResults instanceof Boolean) {
boolean bCalculationResults = (boolean) calculationResults;
if (!bCalculationResults) {
// TODO
}
}
GroovyShell 每次在计算时都会new一次,生成匿名对象,直接占用内存空间,且在一次多线程计算时不会进行内存回收,此时的GroovyShell 对象被认为是可达的。
AppClassLoader是java内置类加载器,用来加载用户应用程序的类。里面有一个parallelLockMap,主要用来存储类锁,避免JVM加载同名的类,和提高类加载的并发度。
GroovyClassLoader如果加载的是无类名的Script,最终会生成一个随机的类名,每次都不一样。导致parallelLockMap不断膨胀, 所以现象在执行脚本频率不算很高的时候一段时间内很难发现内存吃紧。8G的LVS甚至几个月都不会有问题。如果像调度程序这种运行方式,很快就会发现内存泄露。
这应该是一个JDK的BUG,只要加载过多的不同类,parallelLockMap就会不断的膨胀,导致memory leak,最终机器就会宕机。这个这个BUG早已经提给官方,但是JDK7、JDK8都未修复,而且明确指出不修复。
结论:
理论上是不合理的,因为这个Map只进不出。既然官方指出不修复,所以无论是Groovy还是通过别的方式动态加载类,尽量使用固定类名。如果类名是随机的,就要控制加载数量了。
解决办法:
既然无法修改jdk的类加载方式,那就只能从上层规范使用GroovyClassLoader的使用方式。具体为,原来生成script时,都是直接传入脚本表达式,无论表达式是否重复,都新创建了class,造成了内存泄露。我们可以通过某种业务方式将脚本在内存中存储。通过Key-Value的方式,将脚本生成的class或Script对象通过Map方式缓存起来,这样生成的class和script应该是有限的,无论程序运行多少次,都不会存在parallelLockMap无限增加的情况,从而也不会内存泄露。
private static Object invoke(String scriptText) throws Exception {
Script script;
String cacheKey = DigestUtils.md5Hex(scriptText);
if (scriptCache.containsKey(cacheKey)) {
script = scriptCache.get(cacheKey);
} else {
script = groovyShell.parse(scriptText);
scriptCache.put(cacheKey, script);
}
return script.run();
}
private static GroovyShell groovyShell = new GroovyShell();
private static Map<String, Script> scriptCache = new ConcurrentHashMap<>();
// 一直调用invoke即可,每次去取对象时如果有就取,没有从新生成,
// 这样匿名对象是有数量的,不至于会一直增长
String resultFormula = String.format(checkFormula, valueArr);
Object calculationResults = invoke(resultFormula);
if (calculationResults instanceof Boolean) {
boolean bCalculationResults = (boolean) calculationResults;
if (!bCalculationResults) {
// TODO
}
}