1

We are evaluating GroovyShell interpreter (v2.4) in our application for dynamically executing standard Java syntax.

Earlier, we were using Java BeanShell Interpreter for it, but it has an issue under high load which prompted us to look for an alternative such as Groovy.

Sample Java Code

static String script = "int y = x * x; System.out.println(\"** value of y ** :: \" + y ); ";

GroovyShell gs = new GroovyShell();
Script evalScript = gs.parse("void evalMethod() {" + script + "}");
// bind variables
Binding binding = new Binding();
binding.setVariable("x", 5);
evalScript.setBinding(binding);
// invoke eval method
evalScript.invokeMethod("evalMethod", null);

We are seeing thread lock contention when multiple threads execute above code simultaneously. We have multiple threads in the blocked state which is degrading application performance.

Blocked Thread Call Stack

java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.ClassLoader.loadClass(ClassLoader.java:404)
- waiting to lock <0x00000007bc425e40> (a java.lang.Object)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
- locked <0x00000007bd369ba8> (a groovy.lang.GroovyClassLoader)
at groovy.lang.GroovyClassLoader.loadClass(GroovyClassLoader.java:677)
at groovy.lang.GroovyClassLoader.loadClass(GroovyClassLoader.java:545)
at org.codehaus.groovy.control.ClassNodeResolver.tryAsLoaderClassOrScript(ClassNodeResolver.java:185)
at org.codehaus.groovy.control.ClassNodeResolver.findClassNode(ClassNodeResolver.java:170)
at org.codehaus.groovy.control.ClassNodeResolver.resolveName(ClassNodeResolver.java:126)
at org.codehaus.groovy.control.ResolveVisitor.resolveToOuter(ResolveVisitor.java:676)
at org.codehaus.groovy.control.ResolveVisitor.resolve(ResolveVisitor.java:313)
at org.codehaus.groovy.control.ResolveVisitor.transformPropertyExpression(ResolveVisitor.java:845)
at org.codehaus.groovy.control.ResolveVisitor.transform(ResolveVisitor.java:696)
at org.codehaus.groovy.control.ResolveVisitor.transformMethodCallExpression(ResolveVisitor.java:1081)
at org.codehaus.groovy.control.ResolveVisitor.transform(ResolveVisitor.java:702)
at org.codehaus.groovy.ast.ClassCodeExpressionTransformer.visitExpressionStatement(ClassCodeExpressionTransformer.java:142)
at org.codehaus.groovy.ast.stmt.ExpressionStatement.visit(ExpressionStatement.java:42)
at org.codehaus.groovy.ast.CodeVisitorSupport.visitBlockStatement(CodeVisitorSupport.java:37)
at org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitBlockStatement(ClassCodeVisitorSupport.java:166)
at org.codehaus.groovy.control.ResolveVisitor.visitBlockStatement(ResolveVisitor.java:1336)
at org.codehaus.groovy.ast.stmt.BlockStatement.visit(BlockStatement.java:71)
at org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitClassCodeContainer(ClassCodeVisitorSupport.java:104)
at org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitConstructorOrMethod(ClassCodeVisitorSupport.java:115)
at org.codehaus.groovy.ast.ClassCodeExpressionTransformer.visitConstructorOrMethod(ClassCodeExpressionTransformer.java:53)
at org.codehaus.groovy.control.ResolveVisitor.visitConstructorOrMethod(ResolveVisitor.java:201)
at org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitMethod(ClassCodeVisitorSupport.java:126)
at org.codehaus.groovy.ast.ClassNode.visitContents(ClassNode.java:1081)
at org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitClass(ClassCodeVisitorSupport.java:53)
at org.codehaus.groovy.control.ResolveVisitor.visitClass(ResolveVisitor.java:1279)
at org.codehaus.groovy.control.ResolveVisitor.startResolving(ResolveVisitor.java:176)
at org.codehaus.groovy.control.CompilationUnit$12.call(CompilationUnit.java:663)
at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:943)
at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:605)
at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:554)
at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:298)
at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:268)
- locked <0x00000007bd372240> (a java.util.HashMap)
at groovy.lang.GroovyShell.parseClass(GroovyShell.java:688)
at groovy.lang.GroovyShell.parse(GroovyShell.java:700)
at groovy.lang.GroovyShell.parse(GroovyShell.java:736)
at groovy.lang.GroovyShell.parse(GroovyShell.java:727)

My questions are:

  1. Has anyone experienced such problem? If yes, can you please suggest any possible resolution for it? I googled around for this problem and few blogs suggested to cache groovy Script object. Is Script object thread-safe? Moreover, I need to bind variables to Script object (via groovy Binding object, which is different for each execution in different threads), so I don't think caching groovy Script object is a viable option.
  2. Are there any best practices that I need to follow while using Groovy Shell interpreter with Java?
  3. Any other library that we can consider? My requirement is to dynamically execute standard Java syntax.
Aman
  • 1,170
  • 3
  • 15
  • 29

1 Answers1

4

groovy parse&compile is a quite heavy part

if your script is static - then you have to parse it just once

if the scripts always different but repeating - you can cache compiled groovy scripts into

ConcurrentHashMap<String, Class<groovy.lang.Script>>

after

Script evalScript = gs.parse(...);

just put evalScript.getClass() into cache of compiled scripts.

and before gs.parse(...) check if you have compiled script and just take a new instance of the cached class if it exists.

check the following examples and execution speed

String script = "int y = x * x; System.out.println(\"** value of y ** :: \" + y ); ";

def ts = System.currentTimeMillis()
for(int i=0;i<100;i++){
    GroovyShell gs = new GroovyShell();
    Script evalScript = gs.parse("void evalMethod() {" + script + "}");
    // bind variables
    Binding binding = new Binding();
    binding.setVariable("x", i);
    evalScript.setBinding(binding);
    // invoke eval method
    evalScript.invokeMethod("evalMethod", null);
}
println ">> ${System.currentTimeMillis() - ts} millis"

1165 millis

String script = "int y = x * x; System.out.println(\"** value of y ** :: \" + y ); ";
GroovyShell gs = new GroovyShell();
Class<Script> scriptClass = gs.parse("void evalMethod() {" + script + "}").getClass();

def ts = System.currentTimeMillis()
for(int i=0;i<100;i++){
    Script evalScript = scriptClass.newInstance();
    // bind variables
    Binding binding = new Binding();
    binding.setVariable("x", i);
    evalScript.setBinding(binding);
    // invoke eval method
    evalScript.invokeMethod("evalMethod", null);
}
println ">> ${System.currentTimeMillis() - ts} millis"

6 millis

daggett
  • 26,404
  • 3
  • 40
  • 56
  • Thanks! Does caching of compiled script class instances (`evalScript.getClass()`) can cause any memory leak or delayed cleanup of the generated classes and meta classes? – Aman Aug 10 '17 at 18:32
  • if you have unlimited number of scripts, then you have to think about cleaning the cache. instead of `ConcurrentHashMap` you can use something simple: `Collections.synchronizedMap(new WeakHashMap())` or some in-memory cache like [ehcache](http://www.ehcache.org/) or any other.. – daggett Aug 10 '17 at 22:16
  • 1
    Note that this is not thread-safe. Calling setBinding and invokeMethod unsynchronized and parallel may cause unexpected results. Thus, using ConcurrentHashMap is misleading. -> https://stackoverflow.com/questions/63465935/groovyshell-thread-safety – Daniel Alder Aug 18 '20 at 13:04
  • that's correct if you bind defferent params to the same instance. but if you use `scriptClass.newInstance()` - there is no issue. so, concurrentHashMap is relevant. – daggett Aug 18 '20 at 13:33