一、现象描述
通常使用如下代码在Java 中执行 Groovy 脚本:
1 GroovyClassLoader groovyLoader = new GroovyClassLoader(); 2 Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScriptFile); 3 Script groovyScript = groovyClass.newInstance();
时,每次执行groovyLoader.parseClass(groovyScriptFile),Groovy 为了保证每次执行的都是新的脚本内容,会每次生成一个新名字的Class文件对象。但当对同一段脚本每次都执行这个方法时,会导致的现象就是装载的Class会越来越多,从而导致PermGen占用过高,甚至被用满。如下图1所示。
图 1
二、现象分析
visualVM是一款优秀的性能分析工具,可从idea的plugins中下载,具体操作和解释可以看这篇文章 。
由于是PermGen区的泄漏,通过visualVM分析发现类加载部分有大量的GroovyClassLoader,如下图2所示。这时想到有使用两个服务,内部实现是使用Groovy配置的方式(这里我用Groovy封装了一个服务提供的微框架)。但这两个服务其实使用的并不频繁,所以导致切换后的现象不是雪崩式的集群挂掉,而是逐步导致内存占用升高。
图 2
三、现象解决
定位到问题,事情就好办了,首先看一下处理Groovy加载执行的代码;然后为生成的类加一层对象缓存;由于脚本中用到了Binding上下文对象,为了线程安全性,调整执行时的方式。最后问题得到解决。
1. 原始的调用方式
1 Object scriptObject = null; 2 try { 3 Binding binding = new Binding(); 4 binding.setVariable("context", this.context); 5 binding.setVariable("clientInfo", clientInfo); 6 binding.setVariable("params", params); 7 binding.setVariable("data", data); 8
9 GroovyShell shell = new GroovyShell(binding); 10 scriptObject = (Object) shell.evaluate(script); 11 } catch (Throwable t) { 12 log.error("groovy script eval error. script: " + script, t); 13 } 14
15 return scriptObject;
2. 为Groovy Script增加缓存
1 private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>(); 2 ... 3
4 Object scriptObject = null; 5 try { 6 Binding binding = new Binding(); 7 binding.setVariable("context", this.context); 8 binding.setVariable("clientInfo", clientInfo); 9 binding.setVariable("params", params); 10 binding.setVariable("data", data); 11
12 Script shell = null; 13 if (isCached(cacheKey)) { 14 shell = (Script) getCaches().get(cacheKey); 15 } else { 16 shell = new GroovyShell().parse(script); 17 } 18
19 shell.setBinding(binding); 20 scriptObject = (Object) shell.run(); 21
22 // clear
23 binding.getVariables().clear(); 24 binding = null; 25
26 // Cache
27 if (!isCached(cacheKey)) { 28 shell.setBinding(null); 29 getCaches().put(cacheKey, shell); 30 } 31 } catch (Throwable t) { 32 log.error("groovy script eval error. script: " + script, t); 33 } 34
35 return scriptObject;
3. 解决Binding的线程安全问题
1 private Map<String, Object> scriptCache = new ConcurrentHashMap<String, Object>(); 2 ... 3
4 Object scriptObject = null; 5 try { 6 Binding binding = new Binding(); 7 binding.setVariable("context", this.context); 8 binding.setVariable("clientInfo", clientInfo); 9 binding.setVariable("params", params); 10 binding.setVariable("data", data); 11
12 Script shell = null; 13 if (isCached(cacheKey)) { 14 shell = (Script) getCaches().get(cacheKey); 15 } else { 16 shell = cache(cacheKey, script); 17 } 18
19 scriptObject = (Object) InvokerHelper.createScript(shell.getClass(), binding).run(); 20
21 // Cache
22 if (!isCached(cacheKey)) { 23 getCaches().put(cacheKey, shell); 24 } 25 } catch (Throwable t) { 26 log.error("groovy script eval error. script: " + script, t); 27 } 28
29 return scriptObject;