2

I came across an issue with Nashorn, when evaluating a large expression, it works fine in Java 8 , but throws a java.lang.StackOverflowError in Java 11.


    Exception in thread "main" java.lang.StackOverflowError
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.enterBinaryNode(LocalVariableTypesCalculator.java:444)
        at org.openjdk.nashorn.internal.ir.BinaryNode.accept(BinaryNode.java:329)
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.enterJoinPredecessorExpression(LocalVariableTypesCalculator.java:777)
        at org.openjdk.nashorn.internal.ir.JoinPredecessorExpression.accept(JoinPredecessorExpression.java:114)
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.visitExpression(LocalVariableTypesCalculator.java:603)
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.enterBinaryNode(LocalVariableTypesCalculator.java:447)
        at org.openjdk.nashorn.internal.ir.BinaryNode.accept(BinaryNode.java:329)

I came across this question, and in an attempt to fix this issue, as suggested in this comment, I'm trying to use the Standalone Nashorn with Java 11, by using org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory in my code (See this page for info on "why do we need to do that").

I'm having a simple Maven project, where I have added the following to my pom. For simplicity, I've included only the following important parts:

  • Nashorn dependency
  • Maven Compiler Plugin

     <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
            <modelVersion>4.0.0</modelVersion>
        
            <groupId>org.example</groupId>
            <artifactId>maven-project</artifactId>
            <version>1.0-SNAPSHOT</version>
            <packaging>jar</packaging>
        
            <dependencies>
                <dependency>
                    <groupId>org.openjdk.nashorn</groupId>
                    <artifactId>nashorn-core</artifactId>
                    <version>15.3</version>
                </dependency>
            </dependencies>
        
            <build>
                <plugins>
                    ...
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>3.8.0</version>
                        <configuration>
                            <release>11</release>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        
        </project>

(Apart from these, I'm using maven-dependency-plugin and maven-jar-plugin to bundle this into an executable Jar).

The following is my Java code: Main.java

    package main;
    
    import org.openjdk.nashorn.api.scripting.NashornScriptEngine;
    import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
    
    import javax.script.ScriptEngine;
    
    public class Main {
    
        public static void main(String[] args) {
            // write your code here
            String expression = "(\"BUY\" == \"BUY\") && (\"BUY\" == \"BUY\") && (1.00 >= 1000.000)"; // It's a long expression which contains conditions like this.
    
            try {
                NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
                System.out.println("================================================================================");
                System.out.println("factory.engineName: " + factory.getEngineName());
                System.out.println("factory.engineVersion: " + factory.getEngineVersion());
                System.out.println("factory.languageName: " + factory.getLanguageName());
                System.out.println("factory.languageVersion: " + factory.getLanguageVersion());
                System.out.println("================================================================================");
                ScriptEngine engine = factory.getScriptEngine();
    
                Object results = engine.eval(expression);
                System.out.println("Successful");
    
                System.out.println(results);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 

With Java version java version "11.0.9" 2020-10-20 LTS, I'm compiling a jar called maven-project-1.0-SNAPSHOT.jar via doing mvn clean install.

When running the main class of the Jar as follows:


    java -jar target/maven-project-1.0-SNAPSHOT.jar

Based on this information, the standalone version of Nashorn will be loaded with Java 11 since I have used org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory, and therefore, I'm not expecting a java.lang.StackOverflowError with Java 11.

But my output produces a StackOverflowError:

    ================================================================================
    factory.engineName: OpenJDK Nashorn
    factory.engineVersion: 15.3
    factory.languageName: ECMAScript
    factory.languageVersion: ECMA - 262 Edition 5.1
    ================================================================================
    Exception in thread "main" java.lang.StackOverflowError
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.enterJoinPredecessorExpression(LocalVariableTypesCalculator.java:775)
        at org.openjdk.nashorn.internal.ir.JoinPredecessorExpression.accept(JoinPredecessorExpression.java:114)
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.visitExpression(LocalVariableTypesCalculator.java:603)
        at org.openjdk.nashorn.internal.codegen.LocalVariableTypesCalculator.enterBinaryNode(LocalVariableTypesCalculator.java:447)
        at org.openjdk.nashorn.internal.ir.BinaryNode.accept(BinaryNode.java:329)



Update: When compiling this with Java 17 and testing, I did not get a StackOverflowError. As pointed out by @Thorbjørn Ravn Andersen in the comments, the reason to check this was, to rule out the possibility of the in-built Nashorn being picked in Java 11, instead of the standalone one. (Since Java 17 doesn't have the inbuilt Nashorn, it would pick only the standalone one).

Based on this, I can also think of an addition to this question:

how can we make sure that the standalone Nashorn is picked up in Java 11?

I tried executing the jar as follows (referred to this answer of this related question), but the StackOverflowError is still being thrown. (The /path/to/my/maven-project/target/libs/ directory contains jars such as nashorn-core-15.3.jar, asm-7.3.1.jar, asm-commons-7.3.1.jar ...)

java --module-path "/path/to/my/maven-project/target/libs/" --add-modules org.openjdk.nashorn -jar target/maven-project-1.0-SNAPSHOT.jar

(Instead of passing arguments/flags like this, a preferrable way would be to add these as a configuration or so in the pom - and handle it "neater").


FYI, The following code works fine with Java 8, but throws the same StackOverflowError in Java 11, which is why I had to use org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory instead of javax.script.ScriptEngineManager.


    import javax.script.ScriptEngine;
    import javax.script.ScriptEngineManager;
    import javax.script.ScriptException;
    
    public class Main {
    
        public ScriptEngine engine = new ScriptEngineManager().getEngineByName(ENGINE_NAME);;
        public static final String ENGINE_NAME = "nashorn";
    
        public static void main(String[] args) {
            // write your code here
            Main main = new Main();
            Object result = null;
            String exp = "(1640 >= 10) && (1640 <= 100) && (1640 >= 123)";
            try {
                result = main.engine.eval(exp);
                System.out.println(result);
            } catch (ScriptException e) {
                e.printStackTrace();
            }
        }
    }

Am I missing anything here? Thanks in advance for any solutions.

  • 1
    upvoted - you did a great job with background, what you've tried, related links+info – Kaan Jun 16 '22 at 15:18
  • 1
    To rule out that this is due to the two Nashorns (one in the JDK, one standalone) conflicting, could you try to see how things work on Java 17? – Thorbjørn Ravn Andersen Jun 16 '22 at 16:14
  • Hey @ThorbjørnRavnAndersen, thanks, I tried with Java 17 and it worked fine without the Stackoverflow error. What I did was: switched to Java17; Set `17` in the maven-compiler-plugin; Ran `mvn clean install`; and executed the jar. So does this mean that, when I was running with Java 11, there was a conflict between the standalone Nashorn and the inbuilt one from JDK? If so, how can we make it to pick the standalone one? – Senthuran Ambalavanar Jun 16 '22 at 17:07
  • I don’t know. It was a guess based on seeing something similar. It looks like you need to do module magic absolutely correct to get it right. Can the project in question simply be lifted to java 17? – Thorbjørn Ravn Andersen Jun 16 '22 at 17:34
  • Okay, thanks, I'll dig into that. The project cannot be simply lifted to Java 17. This is sort of a proof-of-concept, but the original project must run on Java 11. – Senthuran Ambalavanar Jun 16 '22 at 17:46

1 Answers1

3

How long is the actual source code expression that in the JavaScript code that causes this?

You are not getting the exception in Java 8 because Nashorn in Java 8 didn't have some of the more advanced static type inference capabilities which allow it to generate more optimal code when it can prove some variables will always be ints or doubles and never objects.

This static type inference was added in a later Nashorn version, one released with Java 9. The stack overflow happens in its code as it needs to recursively walk the expression tree. If you have an extremely long expression, this might happen. I'd suggest you break very long expressions into smaller ones, either isolating them into local functions or assigning them to local variables. Local functions are better as then you can keep short-circuited evaluation, e.g. if you had

function f(a, b, c, d, e, f, g, h) {
  return a == b && c == d && e == f && g == h;
}

you can instead do

function f(a, b, c, d, e, f, g, h) {
  function p1() {
    return a == b && c == d;
  }
  function p2() {
    return e == f && g == h;
  }
 
  return p1() && p2();
}

This example just illustrates the technique, Nashorn should be good to evaluate very long expressions before running into a stack overflow.


Alternatively, run your JVM with explicit -Xss setting to give threads more stack memory.

Attila Szegedi
  • 4,405
  • 26
  • 24
  • Thanks @Attila Szegedi. Splitting the expression like this into smaller functions works as expected with JDK 11. In my case, the expression to evaluate is provided by an external application, so I wrote a small method to break that expression first, and create a set of smaller functions like this. So when it comes to performance of the application, now, in addition to the time taken to "just evaluate the expression", there is going to be an additional time to "construct these smaller functions". – Senthuran Ambalavanar Jun 25 '22 at 19:38
  • 1
    @SenthuranAmbalavanar can you also use a higher `-Xss` setting as I suggested at the end of my post? You then won't have a performance hit and won't have to maintain the code for breaking the expressions down. The only drawback is that your app will use more memory per thread. – Attila Szegedi Jun 27 '22 at 07:02