Groovy腳本是應用比較廣泛的一種基於JVM的動態語言,Groovy可以補充Java靜態方面能力的不足。一般語java結合的時候會有三種方式:GroovyClassLoader、GroovyShell和GroovyScriptEngine。
這三種方式用起來差不多,GroovyShell底層也是通過GroovyClassLoader實現的,GroovyScriptEngine是側重多個腳本。
1.GroovyClassLoader
一般這種方式的用法是,調用parse()生成一個groovyClass,然後再去執行腳本。
Class<?> groovyClass = groovyClassLoader.parseClass(textCode);
// 獲得Groovy的實例
GroovyObject groovyTempObject = (GroovyObject)groovyClass.newInstance();
Object object = groovyObject.invokeMethod(methodName, objects);
public Class parseClass(String text) throws CompilationFailedException {
return parseClass(text, "script" + System.currentTimeMillis() +
Math.abs(text.hashCode()) + ".groovy");
}
public Class parseClass(final String text, final String fileName) throws CompilationFailedException {
GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
public GroovyCodeSource run() {
return new GroovyCodeSource(text, fileName, "/groovy/script");
}
});
gcs.setCachable(false);
return parseClass(gcs);
}
public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {
return parseClass(codeSource, codeSource.isCachable());
}
public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException {
synchronized (sourceCache) {
Class answer = sourceCache.get(codeSource.getName());
if (answer != null) return answer;
answer = doParseClass(codeSource);
if (shouldCacheSource) sourceCache.put(codeSource.getName(), answer);
return answer;
}
}
可以看到parseClass的流程,如果沒有傳入腳本名稱,groovy會生成一個新的腳本名稱,這個名稱是和時間戳綁定的。之後會生成一個GroovyCodeSource對象,在doParseClass中去做加載類的動作。所以每次調用parseClass都會生成一個新的class文件。
private Class doParseClass(GroovyCodeSource codeSource) {
validate(codeSource);
Class answer;
CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource());
if (recompile!=null && recompile || recompile==null && config.getRecompileGroovySource()) {
unit.addFirstPhaseOperation(TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber());
}
SourceUnit su = null;
File file = codeSource.getFile();
if (file != null) {
su = unit.addSource(file);
} else {
URL url = codeSource.getURL();
if (url != null) {
su = unit.addSource(url);
} else {
su = unit.addSource(codeSource.getName(), codeSource.getScriptText());
}
}
//生成一個InnerLoader加載器
ClassCollector collector = createCollector(unit, su);
unit.setClassgenCallback(collector);
int goalPhase = Phases.CLASS_GENERATION;
if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT;
unit.compile(goalPhase);
answer = collector.generatedClass;
String mainClass = su.getAST().getMainClassName();
for (Object o : collector.getLoadedClasses()) {
Class clazz = (Class) o;
String clazzName = clazz.getName();
definePackage(clazzName);
//緩存class對象
setClassCacheEntry(clazz);
if (clazzName.equals(mainClass)) answer = clazz;
}
return answer;
}
可以看到在doParseClass方法中,每次都會生成一個InnerLoader加載器,並且會無條件的緩存class對象。
這裏就要提一下groovy的類加載方式了。一般java的雙親委派方式是,Bootstrap <- Extension <-System <-User ClassLoader,所以在groovy這層,在此基礎上會變成Bootstrap <- Extension <-System <- RootLoader <- GroovyClassLoader <-InnerLoader ,當然如果直接用 GroovyClassLoader的方式,就不會有RootLoader加載器。
當項目中大量調用groovy腳本的時候,很容易會出現OOM的問題,這種情況大概率是因爲InnerLoader和class文件都無法在gc的時候被回收,運行較長一段時間後將老年代佔滿,一直觸發fullgc。
我們都知道,一個Class只有滿足以下三個條件,才能被GC回收:
1. 該類所有的實例都已經被GC,也就是JVM中不存在該Class的任何實例;
2. 加載該類的ClassLoader已經被GC;
3. 該類的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法。
顯然classCache被GroovyClassLoader持有,每個class對象都存在引用,無法被gc清理掉。
解決:
(1)外層加個緩存,每次拿groovyClass的時候先從緩存中拿。
這樣可以在一定程度上解決問題。對於同一個名稱的腳本,每次調用緩存中的數據。但是如果時間比較長,不同名稱的腳本數量就非常多了,還是會有gc的問題。
(2)clearCache()
以效率的代價,來解決gc的問題。所以clearCache的時機很重要,不可以太頻繁。