Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ScriptEngine clear and dispose

My application uses a ScriptEngine to offer plugin-ability to my end-users.

ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("nashorn");

Whenever a user makes changes to his script, then the application replaces the engine instane by a new instance.

String newScript = ...;
engine = engineManager.getEngineByName("nashorn");
engine.eval(newScript);

Two closely related questions:

  • Should I try to reuse engines and perform some kind of clear() on them ?

  • If I just replace my engine with a new instance, should I dispose the previous instance in some way, to avoid memory leaks ? (e.g. I can imagine that the user could manage to create a script that starts a thread.)

The problem is, I cannot find any method that looks like a clear() or a dispose(). Does that mean that my current approach is correct ?

like image 994
bvdb Avatar asked Sep 11 '15 09:09

bvdb


2 Answers

You can use a single engine instance but use separate Bindings objects. Bindings acts as a top-level program environment, so if you want to evaluate a script into what is basically a "new global scope" then you could do that. Look into javax.script API docs on how to do this. You can either use ScriptEngine.eval that takes a Bindings as second argument or the one that takes ScriptContext as second argument.

Even if there's no script code surviving from the previous evaluation, you'll save some initialization time as the script engine will already have predefined various JavaScript data-holder classes and property maps ("hidden classes").

Also: yes, everything is garbage collected. There's no need for an explicit "disposal" API.

like image 50
Attila Szegedi Avatar answered Oct 18 '22 09:10

Attila Szegedi


I just wanted to share what I tested myself. It makes perfect sense, but for those still in doubt: Created threads do continue to run if you just replace engine instances:

public static void main(String[] args) throws ScriptException {
    ScriptEngineManager manager = new ScriptEngineManager();

    String script =
            "new java.lang.Thread(function() {\n" +
            "  for(;;) {" +
            "    print('Here\\'s Johnny !');" +
            "    java.lang.Thread.sleep(1000);" +
            "  }\n" +
            "}).start();";

    ScriptEngine engine = manager.getEngineByName("nashorn");
    try {
        engine.eval(script);
    } catch (ScriptException e) {
        e.printStackTrace();
    }
    
    // replace engine
    engine = manager.getEngineByName("nashorn");
    engine.eval("print('please, make it stop!!!');");
    
    // please collect !!!
    System.gc();
}

Output:

    Here's Johnny !
    please, make it stop!!!
    Here's Johnny !
    Here's Johnny !
    Here's Johnny !
    ...

I guess that the garbage collector can clean the scripts, but not their actions outside their context. I think created threads are not even linked to the scripts in any way (i.e. outside their scope). So, I think it's just impossible for the jvm to detect or decide that these threads are linked to a replaced script and may or may not be stopped.

But this leads us too far for one stackoverflow question. Let's just focus on the ability to dispose/clear the bindings (i.e. ScriptContext).

Block java threads in nashorn scripts:

A possible solution, is to narrow down the available functionality. Here follow a couple of ways to avoid the creation of threads:

The following disables all java functionality:

// the option -nj is short for --no-java
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine("-nj");

But you can also disable specific classes, using a ClassFilter.

    ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine((className) -> {
        if ("java.lang.Thread".equals(className)) return false;
        if ("java.lang.Runnable".equals(className)) return false;
        if ("java.util.Timer".equals(className)) return false;
        if (className.startsWith("java.util.concurrency")) return false;
        if (className.startsWith("javafx")) return false;
        if (className.startsWith("javax.swing")) return false;
        if (className.startsWith("java.awt")) return false;
        return true;
    });

Note: as soon as you define a ClassFilter also reflection classes are blocked automatically. So, you don't have to block those packages explicitly.

like image 33
bvdb Avatar answered Oct 18 '22 07:10

bvdb