`
RednaxelaFX
  • 浏览: 3015127 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

在Java里整合Groovy脚本的一个陷阱

阅读更多
最近在项目里要在Java中整合Groovy脚本来粘合各个组件/服务,所以这两天在测试几种整合方法。最初是想用JSR 223系的API,不过我们这边对ClassLoader有特别需求,JSR 223的API满足不了,所以还是转而考虑Groovy自身的整合机制。

除了BSFJSR 223之外,整合Groovy基本上有三种途径:GroovyShell(以及Eval)、GroovyClassLoaderGroovyScriptEngine。这些在官网的Embedding Groovy文档上有所描述,在几本Groovy的书里也有提及。

然而在整合Groovy脚本的时候可能会遇到一类陷阱:临时加载的类未能及时被释放,进而导致PermGen OutOfMemoryError;没那么严重的时候也会引发比较频繁的full GC从而影响稳定运行时的性能。

如果只是要执行一些Groovy脚本,那么GroovyShell看来是个不错的选择。于是用它做个小测试:
(环境在后面的截图里有写,这里就不详细说了。Windows XP SP3/Sun JDK 1.6.0u18/client默认参数/Groovy 1.7.1)
package fx.test;

import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.io.IOException;

/**
 * @author sajia
 *
 */
public class TestGroovyShell {
    // see if the number of loaded class keeps growing when
    // using GroovyShell.parse
    public static void test() {
        GroovyShell shell = new GroovyShell();
        String scriptText = "def mul(x, y) { x * y }\nprintln mul(5, 7)";
        
        while (true) {
            Script script = shell.parse(scriptText);
            Object result = script.run();
        }
    }
    
    public static void main(String[] args) {
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        test();
    }
}

启动这个程序,按一下回车,放着跑不到一分钟就会看到异常:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
	at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:55)
	at groovy.lang.GroovyClassLoader$ClassCollector.createClass(GroovyClassLoader.java:496)
	at groovy.lang.GroovyClassLoader$ClassCollector.onClassNode(GroovyClassLoader.java:513)
	at groovy.lang.GroovyClassLoader$ClassCollector.call(GroovyClassLoader.java:517)
	at org.codehaus.groovy.control.CompilationUnit$11.call(CompilationUnit.java:767)
	at org.codehaus.groovy.control.CompilationUnit.applyToPrimaryClassNodes(CompilationUnit.java:971)
	at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:519)
	at org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:497)
	at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:474)
	at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:292)
	at groovy.lang.GroovyShell.parseClass(GroovyShell.java:727)
	at groovy.lang.GroovyShell.parse(GroovyShell.java:739)
	at groovy.lang.GroovyShell.parse(GroovyShell.java:766)
	at groovy.lang.GroovyShell.parse(GroovyShell.java:757)
	at fx.test.TestGroovyShell.test(TestGroovyShell.java:20)
	at fx.test.TestGroovyShell.main(TestGroovyShell.java:31)

如果在启动这个测试时加上-verbose选项,可以看到每次执行GroovyShell.parse()方法时都会打印出这样的日志:
[Loaded Script183 from file:/groovy/shell]
[Loaded Script183$mul from file:/groovy/shell]

也就是说上面测试中的脚本每次被parse()都新生成两个类,一个对应顶层代码,一个对应其中的mul()方法。在循环中调用parse()方法,不消一会儿就把HotSpot的PermGen给撑爆了;虽然执行过程中也可以看到PermGen的空间紧张经常引发full GC,而在full GC时会卸载掉许多不再有引用的类,但这个测试中卸载的速度没有生成的速度快,就杯具了。

除了类自身之外,类中的常量池所引用的字符串也都需要被intern,上面的例子中像"mul"这个名字就会被intern掉;在HotSpot中,intern的String实例也是在PermGen上分配空间的。内容相同的字符串就算被intern很多次在PermGen的字符串池里也只会有一份,不过如果连续执行很多脚本,脚本里在“成员”和“类型”级别上出现了很多不同的标识符的话,这也会对字符串池造成压力。

用JConsole可以形象的看到PermGen爆掉的过程。下面两张截图中右边骤然下降的线是在测试程序抛出异常而终止后JConsole与之连接被断开的时候的,可以忽略掉。






(补一张PermGen趋势截图)


==========================================================================

Sun JDK 1.6.0u18的HotSpot在32位Windows XP SP3上默认选用client模式,默认PermGen大小是64MB。如果在上面的测试里给入参数-XX:MaxPermSize=512m,将PermGen最大大小设置到512MB,情况会怎样呢?放着让它多跑几分钟,会看到:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOfRange(Unknown Source)
        at java.lang.String.<init>(Unknown Source)
        at java.lang.StringBuffer.toString(Unknown Source)
        at java.net.URLStreamHandler.toExternalForm(Unknown Source)
        at java.net.URL.toExternalForm(Unknown Source)
        at java.net.URL.toString(Unknown Source)
        at java.lang.ClassLoader.defineClassSourceLocation(Unknown Source)
        at java.lang.ClassLoader.defineClass(Unknown Source)
        at org.codehaus.groovy.reflection.ClassLoaderForClassArtifacts.define(ClassLoaderForClassArtifacts.java:27)
        at org.codehaus.groovy.reflection.ClassLoaderForClassArtifacts$1.run(ClassLoaderForClassArtifacts.java:71)
        at org.codehaus.groovy.reflection.ClassLoaderForClassArtifacts$1.run(ClassLoaderForClassArtifacts.java:69)
        at java.security.AccessController.doPrivileged(Native Method)
        at org.codehaus.groovy.reflection.ClassLoaderForClassArtifacts.defineClassAndGetConstructor(ClassLoaderForClassArtifacts.java:69)
        at org.codehaus.groovy.runtime.callsite.CallSiteGenerator.compilePojoMethod(CallSiteGenerator.java:227)
        at org.codehaus.groovy.reflection.CachedMethod.createPojoMetaMethodSite(CachedMethod.java:244)
        at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.createCachedMethodSite(PojoMetaMethodSite.java:158)
        at org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite.createPojoMetaMethodSite(PojoMetaMethodSite.java:147)
        at groovy.lang.MetaClassImpl.createPojoCallSite(MetaClassImpl.java:2994)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.createPojoSite(CallSiteArray.java:114)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.createCallSite(CallSiteArray.java:148)
        at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:40)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117)
        at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
        at org.codehaus.groovy.ast.builder.AstBuilderInvocationTrap.visitMethodCallExpression(AstBuilderTransformation.groovy:179)
        at org.codehaus.groovy.ast.expr.MethodCallExpression.visit(MethodCallExpression.java:67)
        at org.codehaus.groovy.ast.CodeVisitorSupport.visitExpressionStatement(CodeVisitorSupport.java:69)
        at org.codehaus.groovy.ast.stmt.ExpressionStatement.visit(ExpressionStatement.java:40)
        at org.codehaus.groovy.ast.CodeVisitorSupport.visitBlockStatement(CodeVisitorSupport.java:35)
        at org.codehaus.groovy.ast.stmt.BlockStatement.visit(BlockStatement.java:51)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)


如果看JConsole监视到的类加载状况,会看到:

右边陡然下降的曲线跟上一个测试一样是在抛了异常之后的部分,可以忽略。

中间一段看起来很平,看来是没问题?
其实不然。如果结合程序的执行速度与GC消耗的时间来看,会发现加载类的数量的曲线比较平的这段时间里,上面测试代码的每轮循环都要等很久才会输出一个35,而大部分时间都消耗在了full GC上;这是由于“某种原因”(*)使得GC堆的年老代非常满,于是稍微分配一点空间就要触发full GC。最终GC堆还是没撑住,就爆了。
也就是说这次没有让PermGen爆掉只不过是因为瓶颈转移到别的部分了而已。

*:这个“某种原因”以后或许会发篇帖分析一下。这篇就只谈谈现象吧。

==========================================================================

GroovyShell上的几个方法都有同样的问题,像是evaluate()的各个重载、parse(),还有Eval.me()/x/xy()/xyz()这些方法都一样。

当然,在上面的测试中只要把shell.parse(scriptText);这句移到循环的外面就可以避免撑爆PermGen的问题——因为只调用了一次parse()方法,相应的也就只生成了对应的那些新的类。
于是这里就有个启示:如果嵌入GroovyShell的场景需要经常执行Groovy脚本,那么或许应该通过weak cache来检查先前是不是已经处理过当前输入的脚本,没处理过的时候才去调用GroovyShell.parse()并将脚本记录到weak cache里。

==========================================================================

如果GroovyShell可能导致PermGen问题,那GroovyClassLoader是不是也一样会呢?换用下面的代码来测试的话:

package fx.test;

import groovy.lang.GroovyClassLoader;

import java.io.IOException;

/**
 * @author sajia
 *
 */
public class TestGroovyClassLoader {
    // see if the number of loaded class keeps growing when
    // using GroovyClassLoader.parseClass
    public static void test() {
        GroovyClassLoader loader = new GroovyClassLoader();
        String scriptText = "class Foo {\n"
            + "  int add(int x, int y) { x + y }\n"
            + "}";

        Class<?> clazz = null;
        while (true) {
            Class<?> newClazz = loader.parseClass(scriptText);
            if (clazz == newClazz) {
                System.out.println("class cached");
                break;
            }
            clazz = newClazz;
        }
    }
    
    public static void main(String[] args) {
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        test();
    }
}

却发现它跑起来没导致PermGen OOM。

同样看看JConsole监控的截图:




可以看到虽然被加载的类仍然非常多,但多数都及时被卸载了所以PermGen能动态维持在一个不太满的水平上。
观察-verbose得到的日志,可以看到上面例子中每次调用GroovyClassLoader.parseClass()只生成并加载了一个类:
[Loaded Foo from file:/groovy/script]


虽然每次生成并加载的类的数量比GroovyShell.parse()的少,但这个测试总觉得缺了点什么。对,没对那些新生成的类生成过实例。那么改一下,加上对Class.newInstance()的调用:
Class<?> clazz = null;
while (true) {
    Class<?> newClazz = loader.parseClass(scriptText);
    try {
        newClazz.newInstance(); // make new instance!
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (clazz == newClazz) {
        System.out.println("class cached");
        break;
    }
    clazz = newClazz;
}

则类加载与PermGen的表现又有所不同了:



虽然还是没有因为PermGen而OOM,但PermGen的压力明显比不调用newInstance()时高了些。

接下来,模仿我们这边已有的一个项目里对Groovy的用法,加上对新生成的实例的方法调用再来测试一下:
public static void test() {
    String scriptText = "class Foo {\n"
        + "  int add(int x, int y) { x + y }\n"
        + "}";

    Class<?> clazz = null;
    while (true) {
        GroovyClassLoader loader = new GroovyClassLoader();
        Class<?> newClazz = loader.parseClass(scriptText);
        try {
            Object obj = newClazz.newInstance();
            Object i = obj.getClass()
                .getMethod("add", int.class, int.class)
                .invoke(obj, 2, 3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (clazz == newClazz) {
            System.out.println("class cached");
            break;
        }
        clazz = newClazz;
    }
}

结果也还正常,跑了十几分钟都没有OOM,也没有表现出OOM的倾向。Good。

==========================================================================

说来GroovyShell里还特别写了注释说不缓存脚本:
    private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
        // Don't cache scripts
        return loader.parseClass(codeSource, false);
    }

不乱缓存东西或许也算是一种美德吧……?

GroovyShell.parse()内部其实也就是调用GroovyClassLoader.parseClass()去解析Groovy脚本并生成Class实例(会是groovy.lang.Script的子类),然后调用Class.newInstance()构造出一个新的实例以Script类型的引用返回出来。

既然它默认不缓存东西,怎么上面的例子里用它就会PermGen OOM而直接用GroovyClassLoader就没事呢?看来是两个例子中脚本的内容不同带来了差异。不过换成下面的版本来测却并没出问题:
package fx.test;

import groovy.lang.GroovyClassLoader;
import groovy.lang.Script;

import java.io.IOException;

/**
 * @author sajia
 *
 */
public class TestGroovyClassLoader {
    // see if the number of loaded class keeps growing when
    // using GroovyClassLoader.parseClass
    public static void test() {
        String scriptText = "def mul(x, y) { x * y }\nprintln mul(5, 7)";

        while (true) {
            GroovyClassLoader loader = new GroovyClassLoader();
            Class<?> newClazz = loader.parseClass(scriptText);
            try {
                Object obj = newClazz.newInstance();
                Script script = (Script) obj;
                script.run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        test();
    }
}


用GroovyShell的时候什么地方挂住了什么不该挂住的引用么……?
下次再找原因吧……
  • 大小: 139.5 KB
  • 大小: 144.9 KB
  • 大小: 146.4 KB
  • 大小: 131.6 KB
  • 大小: 156 KB
  • 大小: 142.8 KB
  • 大小: 161.5 KB
  • 大小: 139.7 KB
  • 大小: 138.8 KB
分享到:
评论
10 楼 LinApex 2014-04-12  
good,一直对 groovy 性能这块有担忧,帮我解决了一点疑问
9 楼 bo_hai 2013-05-06  
8 楼 beneo 2013-01-06  
不过有个#resetLoadedClasses这个函数
7 楼 beneo 2013-01-06  
我错了,还是有这个情况,GroovyShell
6 楼 beneo 2013-01-06  
2.0.5 groovy-all

测试 GroovyShell 不会有 OOM 了
5 楼 我改名了 2012-09-28  
写的不错,支持 。
4 楼 RednaxelaFX 2011-12-29  
scholers 写道
请问下有没有好的方法调试脚本的内容?
就是你文中提到的groovyclassloader加载进去的脚本

我也没啥现成的好办法。用JDI来写一个是能做到的但是好麻烦啊(远目
3 楼 scholers 2011-12-29  
请问下有没有好的方法调试脚本的内容?
就是你文中提到的groovyclassloader加载进去的脚本
2 楼 RednaxelaFX 2010-03-20  
JohnnyJian 写道
我刚好准备下一篇博客写关于GroovyClassLoader的内容

耶,期待一下~~
1 楼 JohnnyJian 2010-03-20  
我刚好准备下一篇博客写关于GroovyClassLoader的内容

相关推荐

Global site tag (gtag.js) - Google Analytics