`
RednaxelaFX
  • 浏览: 3053248 次
  • 性别: 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的内容

相关推荐

    Java整合Groovy脚本笔记

    Java整合Groovy脚本,Java整合Groovy脚本Java整合Groovy脚本,Java整合Groovy脚本Java整合Groovy脚本,Java整合Groovy脚本Java整合Groovy脚本,Java整合Groovy脚本Java整合Groovy脚本,Java整合Groovy脚本Java整合...

    Java调用Groovy,实时动态加载数据库groovy脚本

    我们可以编写一个Java方法,从MongoDB中查询到Groovy脚本,然后利用上述的Groovy调用机制执行这些脚本。 例如,我们可能会有一个MongoDB集合,其中每个文档包含一个字段存储Groovy脚本。Java代码会连接到MongoDB,...

    干货:Jenkins Pipeline调用shell、python、java、groovy脚本的正确使用姿势.doc

    例如在服务器上执行 shell 命令、运行 python 或者 java 测试代码以及使用 groovy 脚本来实现一些更为复杂的功能等等。 执行 shell 命令 在 Jenkins Pipeline 中,使用 `sh` 指令可以执行 shell 命令。例如,使用 ...

    基于groovy实现 java脚本动态编译、部署、发布;可以通过脚本直接调用dubbo接口.zip

    在调用Dubbo接口方面,Dubbo是一个高性能、轻量级的Java RPC框架。Groovy脚本可以通过Java API直接调用Dubbo服务,就像在Java代码中一样。首先,你需要在脚本中导入Dubbo的相关依赖,然后创建Dubbo的消费者...

    java 动态脚本语言 精通 Groovy

    Groovy是一种基于Java平台的动态脚本语言,它在Java开发者中越来越受欢迎,因为它提供了简洁、灵活的语法,以及强大的动态编程能力。Groovy与Java兼容性极佳,可以直接调用Java类库,使得它在Java生态系统中具有广泛...

    groovy和Java相互调用1

    标题中的“Groovy和Java相互调用1”指的是在编程时如何在Groovy语言环境中调用Java类,以及反之,如何在Java程序中调用Groovy类。这是一种跨语言交互的方式,特别是在混合使用Groovy和Java的项目中非常常见。 ...

    Java中使用Groovy的三种方式

    在Java开发中,Groovy是一种强大的、动态类型的脚本语言,它可以无缝地与Java代码集成,为开发者提供了更简洁、灵活的语法。本文将深入探讨在Java项目中使用Groovy的三种主要方式,并阐述它们各自的优势和应用场景。...

    Groovy脚本:Java平台的动态编程利器

    Groovy是一种运行在Java平台上的动态脚本语言,它与Java紧密集成,提供了一种简洁、灵活的方式来编写Java代码。Groovy不仅支持静态类型检查,还可以在运行时动态地编译和执行代码,这使得它在Java平台中的应用场景...

    groovy脚本执行工具.zip

    总的来说,这个“groovy脚本执行工具”是一个对于Java开发者非常有用的工具,它简化了Groovy脚本的执行流程,便于在日常开发和测试中快速使用Groovy。无论你是新手还是经验丰富的开发者,都可以利用这个工具快速上手...

    groovy脚本实现对数据库的增删改查

    在实际项目中,Groovy还常与其他工具结合,如Grails(一个基于Groovy的Web框架),或者Gradle(构建自动化工具)。这些工具也支持Groovy脚本,使数据库操作更加灵活和高效。总的来说,掌握Groovy进行数据库操作是...

    Groovy脚本:Java平台自动化测试的灵活选择

    在自动化测试领域,Groovy脚本以其在Java平台上的卓越性能和灵活性,成为了开发者的热门选择。Groovy是一种基于JVM的动态编程语言,它不仅继承了Java的强大功能,还引入了动态类型、闭包、DSL等现代编程特性,使得...

    hugo110-java_run_groovy-master_java_

    标题“hugo110-java_run_groovy-master_java_”表明这是一个关于使用Java运行Groovy脚本的项目,可能是某个GitHub仓库的克隆或归档。描述中提到,这个环境是为了方便用户(可能是开发者)实践Groovy语言,并为后续的...

    JVM 动态执行Groovy脚本的方法

    - 获取 ScriptEngine 实例:通过 ScriptEngineManager 获取一个脚本引擎 ScriptEngine 对象。 - 执行脚本:使用 ScriptEngine 的 eval 方法执行Groovy脚本。 - javax.script 包结构:javax.script 包中定义了脚本...

    java在嵌入运行groovy代码1

    这里创建了一个 `list` 变量,并在 Groovy 脚本中定义了一个方法 `call()`,然后通过 `evaluate` 调用这个方法。 如果你的 Groovy 脚本是一个完整的类结构,包括 `main` 方法,你可以使用 `GroovyShell` 来执行 `...

    java调用脚本语言笔记(jython,jruby,groovy)

    Jython是Python的一个Java实现,它允许Java开发者利用Python的强大语法和丰富的库。要使用Jython,首先需要在项目中引入Jython的jar包。然后,你可以通过`ScriptEngineManager`和`ScriptEngine`接口来执行Jython脚本...

    Java/groovy脚本程序调用被测试应用bean

    脚本通过上下文applicationcontext获取被测试应用bean,从而可以进行接口内容获取等操作。目前该方式主要运用到性能平台脚本搭建、代码深度测试、接口测试、白盒测试等多方面。...支持Java、groovy脚本。

    groovy脚本语言bin

    总之,Groovy脚本语言bin提供的1.6.5版本是一个全面的开发包,旨在支持开发人员在JVM上快速开发、测试和部署Groovy应用程序。无论是对于新手还是有经验的Java开发者,Groovy都提供了一种强大而灵活的工具,以提升...

    SpringBoot-Gradle-Maven-Java-Groovy

    1. **SpringBoot**: SpringBoot是Spring框架的一个模块,旨在简化Spring应用的初始搭建以及开发过程。它提供了一种快速构建可运行的应用程序的方式,内置了Tomcat服务器和默认配置,使得开发者无需过多关注配置细节...

    Java Groovy

    - **定义**:Groovy是一种面向对象的编程语言,它直接运行在Java平台上,并且能够与现有的Java代码无缝集成。 - **特点**: - **语法简洁**:Groovy提供了更为简洁、动态的语法特性,使得编写代码变得更加高效。 -...

    java groovy整合

    1、eclipse安装groovy的插件。 2、创建java project。 3、把groovy-all-2.1.9.jar,加入...4、编写hello.groovy脚本文件,并在java代码中调用脚本文件 http://blog.csdn.net/bolg_hero/article/details/19077981

Global site tag (gtag.js) - Google Analytics