Groovy的classloader加載機制引起的頻繁GC

[b]GROOVY的classloaer機制 [/b]
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]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章