[b]GROOVY的classloaer机制 [/b]
Groovy嵌入到JAVA里面执行有一种方式在通过使用GroovyClassLoader将Groovy的类动态地载入到Java程序中并直接使用或运行它.
例如:
产生脚本:
[b]频繁GC的问题[/b]
项目环境:groovy1.8.4 + jdk1.6
在一次应用开发中被性能测试同学发现有内存泄漏,内存回收异常。每4分钟FGC一次。并且由old区内存情况发现内存泄露现象;经定位,发现在OLD区有大量的GROOVY脚本存在,导致频繁FULL GC;在GROOVY的脚本执行代码里面,当从CACHE里面获取到groovy脚本的字符串时,调用
解析生成groovy脚本,GroovyClassLoader是GROOVY自带的类加载器,继承JAVA的URLClassLoader,其实质就是将GROOVY脚本变成class,这个过程会消耗CPU和内存,同时由于GROOVY在加载每个脚本的时候,都在脚本前面增加了
的代码,导致对任何一次脚本解析都产生一个新的脚本,这样反应在页面上就是相当于每刷新一次,就会产生一批新的脚本,当做性能测试,压上很多用户的时候,就会导致大量的脚本对象产生,从而导致了OLD存在大量的groovy script脚本,最终引起频繁的GC(每4分钟一次);
解决的方式是在程序里面采用一个全局的MAP,对于同样的groovy 的script脚本,只调用
一次,然后将生成的script对象存放在map中,这样来避免每个脚本每次调用都产生新的script对象;修改之后,在同样的性能测试条件下,每1.5小时也没有full gc;
其实仔细想想,groovy用这种方式来保持其动态性,每次动态加载class(or脚本),这样的话当修改了脚本之后,立即生效;但是这样的机制必然带来的是性能的损耗。
下面的代码测试了GROOVY动态执行带来的成本损耗:
执行的结果:
[color=red]
[b]没有缓存的代码的执行时间是有缓存的600倍,这个差距还是有点大的;[/b][/color]
Groovy嵌入到JAVA里面执行有一种方式在通过使用GroovyClassLoader将Groovy的类动态地载入到Java程序中并直接使用或运行它.
例如:
ClassLoader parent = getClass().getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
Class groovyClass = loader.parseClass(new File("src/main/groovy/script/Test.groovy"));
// 调用实例中的某个方法
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
Object[] args = {};
groovyObject.invokeMethod("run", args);
产生脚本:
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class<?> scriptClass =
groovyClassLoader.parseClass(scriptCode);
return InvokerHelper.createScript(scriptClass, binding);
[b]频繁GC的问题[/b]
项目环境:groovy1.8.4 + jdk1.6
在一次应用开发中被性能测试同学发现有内存泄漏,内存回收异常。每4分钟FGC一次。并且由old区内存情况发现内存泄露现象;经定位,发现在OLD区有大量的GROOVY脚本存在,导致频繁FULL GC;在GROOVY的脚本执行代码里面,当从CACHE里面获取到groovy脚本的字符串时,调用
scriptClass = groovyClassLoader.parseClass(scriptCode)
解析生成groovy脚本,GroovyClassLoader是GROOVY自带的类加载器,继承JAVA的URLClassLoader,其实质就是将GROOVY脚本变成class,这个过程会消耗CPU和内存,同时由于GROOVY在加载每个脚本的时候,都在脚本前面增加了
return parseClass(text, "script" + System.currentTimeMillis() +
Math.abs(text.hashCode()) + ".groovy");
的代码,导致对任何一次脚本解析都产生一个新的脚本,这样反应在页面上就是相当于每刷新一次,就会产生一批新的脚本,当做性能测试,压上很多用户的时候,就会导致大量的脚本对象产生,从而导致了OLD存在大量的groovy script脚本,最终引起频繁的GC(每4分钟一次);
解决的方式是在程序里面采用一个全局的MAP,对于同样的groovy 的script脚本,只调用
scriptClass = groovyClassLoader.parseClass(scriptCode)
一次,然后将生成的script对象存放在map中,这样来避免每个脚本每次调用都产生新的script对象;修改之后,在同样的性能测试条件下,每1.5小时也没有full gc;
其实仔细想想,groovy用这种方式来保持其动态性,每次动态加载class(or脚本),这样的话当修改了脚本之后,立即生效;但是这样的机制必然带来的是性能的损耗。
下面的代码测试了GROOVY动态执行带来的成本损耗:
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;
import java.util.HashMap;
import java.util.Map;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.junit.Test;
public class BaseTestCase {
private static Map<String, Script> scripts = new HashMap<String, Script>();
@Test
public void test() {
// fail("Not yet implemented");
long time1 = test_execute(false);
System.out
.println("------------------------------------------------------\n");
long time2 = test_execute(true);
System.out.println(time1 / time2);
}
public long test_execute(boolean isCached) {
// groovy脚本
String scriptStr = "def execute(Map temp){return [\"result\":\"hello world\" + temp.a]}";
// def code = new Source(source: script,type: "new",name: "hello")
Map<String, Object> context = new HashMap<String, Object>();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
Map<String, Object> params = new HashMap<String, Object>();
params.put("a", "b" + i);
excute(context, params, scriptStr, isCached);
}
long time = System.currentTimeMillis() - start;
System.out.println("take time: " + time);
return time;
}
/**
* 将脚本源码分析成Script对象
*
* @param key
* 将作为class name
* @param scriptCode
* 脚本源码
* @return script对象
*/
private Script parseScript(String[] args, String scriptCode,
boolean isCached) {
try {
Script script = scripts.get("1");
if (!isCached || script == null) {
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class<?> scriptClass = groovyClassLoader.parseClass(scriptCode);
Binding context = new Binding(args);
script = InvokerHelper.createScript(scriptClass, context);
}
if(isCached){
scripts.put("1", script);
}
return script;
} catch (Throwable e) {
return null;
}
}
private void excute(Map<String, Object> contentx,
Map<String, Object> params, String code, boolean isCached) {
String[] args = new String[] { "aaa", "ddd" };
Script script = parseScript(args, code, isCached);
Map<String, Object> result = (Map<String, Object>) script.invokeMethod(
"execute", params);
for (Map.Entry<String, Object> entry : result.entrySet()) {
System.out.println(entry.getValue());
}
}
}
执行的结果:
take time: 9585
------------------------------------------------------
take time: 16
599
[color=red]
[b]没有缓存的代码的执行时间是有缓存的600倍,这个差距还是有点大的;[/b][/color]