Groovy腳本的OOM源碼分析

 

   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的時機很重要,不可以太頻繁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章