Nashorn ist die neue JVM-basierte JavaScript-Implementierung im JDK 8

Java 8 & Nashorn: Der Weg zur polyglotten VM

Marcus Lagergren, Wolfgang Weigend
©shutterstock.com/Master3D

Seit Mitte Dezember 2012 ist das Projekt Nashorn im OpenJDK angekommen, mit dem Ziel eine zu hundert Prozent in Java geschriebene, schlanke und performanceoptimierte JavaScript-Engine direkt auf der JVM zu implementieren. Den Entwicklern soll Nashorn ermöglichen, unter Berücksichtigung von JSR 223 (Scripting for the Java Platform), JavaScript in Java-Anwendungen einzubinden und eigene JavaScript-Anwendungen zu erstellen, die bisher jrunscript verwendet haben und künftig das neue Command-line-Werkzeug jjs (JavaScript in Java) im JDK 8 benutzen können.

Das Nashorn-Projektdesign nutzt den Vorteil einer komplett neuen Codebasis und fokussiert sich auf moderne Technologien für native JVMs mittels Method Handles und invokedynamic-APIs, wie im JSR 292 (Supporting Dynamically Typed Languages on the Java Platform) beschrieben.

Motivation für das Projekt Nashorn

Die fünfzehn Jahre alte Rhino-Engine ist nicht mehr zeitgemäß und bietet hauptsächlich Plug-in-Fähigkeit über das Package javax.script. Das bisherige Scripting-API besteht aus Interfaces und Klassen zur Definition der Java-Scripting-Engine und liefert ein Framework zur Verwendung dieser Schnittstellen und Klassen innerhalb von Java-Anwendungen. Für Rhino bleibt trotz seines Alters und seiner Schwächen das einzige Argument übrig, als Bindeglied den Zugriff zur großen und umfassenden JDK-Bibliothek herzustellen. Die Motivation für Nashorn, als Bestandteil von JDK 8, liegt in der Unterstützung von JavaScript, die Java-/JavaScript-Integration und die JVM für dynamisch typisierte Skriptsprachen attraktiv zu machen. Dies beruht auf den bereits in Java SE 7 eingeführten invokedynamic-Instruktionen, bei denen es darum geht, den Bytecode invokedynamic zu verwenden, um Methodenaufrufe von Skriptsprachen in der JVM erheblich zu beschleunigen. Das Package java.lang.invoke enthält dynamische Sprachunterstützung, die direkt von den Java-Core-Klassenbibliotheken und der VM bereitgestellt wird. JavaScript oder andere JVM-Sprachen können von den Vorteilen der darunterliegenden JVM, inklusive Memory-Management, Codeoptimierung und den Grundeigenschaften vom JDK profitieren, wie in Abbildung 1 dargestellt.
Die Nashorn-Engine ist hundert Prozent ECMAScript-kompatibel und hat alle notwendigen Kompatibilitätstests für ECMAScript 5.1 bestanden. Auch die nächste Version ECMAScript 6.0 wird bereits vom Entwicklungsteam genau verfolgt, um eine konforme Implementierung der Skriptsprache mit Unterstützung aller Typen, Werte, Objekte, Funktionen, Programmsyntax und Semantik in Nashorn sicherzustellen.

Abb. 1: Java-Bytecode in der JVM

Nashorn im Leistungsvergleich

Der direkte Vergleich von Nashorn und Rhino, ermittelt durch die Octane-Benchmark-Suite, einer Micro-Benchmark-Suite im Single-Thread-Modus, zeigt auf einem iMac mit OS X und Intel-Quad-Core-Prozessor i7 (3.4 GHz) eine deutliche Leistungsverbesserung der Nashorn-Engine (Abb. 2). Die Darstellung vom Graph wurde dabei auf die Rhino-Leistungsfähigkeit normalisiert, und es ist erkennbar, dass sich die Leistungsklasse von Nashorn in einer anderen Größenordnung befindet. Dennoch sollten die Benchmark-Ergebnisse nicht überbewertet werden, weil der Octane-Benchmark nicht den optimalen Leistungsvergleich zur Bestimmung der JavaScript-Geschwindigkeit liefert, sondern hauptsächlich die Bytecode-Ausführungsgeschwindigkeit ermittelt wird. Jedoch sind einige Testläufe interessant, beispielsweise Splay, weil dabei ein großer Anteil der Leistung vom GC-Durchsatz innerhalb der JVM hergeleitet wurde und die Ergebnisse im Vergleich zur Google-V8-JavaScript-Engine gleichwertig sind. Die vielversprechenden Leistungsmerkmale von Nashorn sind unter anderem invokedynamic zu verdanken und zeigen auf, dass sich die JVM in Richtung einer polyglotten Laufzeitumgebung bewegt. Die Herausforderung für Nashorn liegt in der Einbettung von Scripting in die Java Plattform (JSR 223) mit einer akzeptablen Geschwindigkeit.

Abb. 2: Rhino vs. Nashorn im Vergleich

Aufmacherbild: Abstract black and white striped background von Shutterstock / Urheberrecht: Master3D

[ header = Seite 2: JSR 292 invokedynamic als Basis für Nashorn ]

JSR 292 invokedynamic als Basis für Nashorn

Der Trend, dass einige neue dynamisch typisierte Sprachen (Non-Java-Programmiersprachen) die JVM als Laufzeitumgebung verwenden, wurde in erheblichem Maß durch invokedynamic gefördert und führte dazu, dass sich eine höhere Performance vom Bytecode erzielen lässt, als zuvor möglich war. Bisher war der Bytecode vorrangig für das Java-Programmiermodell mit strenger Typisierung und anderen nativen Java-Konzepten ausgelegt, was sich jetzt durch invokedynamic ändert und den Vorteil einer tauglichen Implementierung von dynamischen Sprachen auf der JVM ermöglicht.

Auf der JAX führte unsere Redakteurin Claudia Fröhling ein Interview mit Marcus Lagergren von Oracle zum Thema Nashorn und invokedynamic.

Erstmalig in der Geschichte der JVM-Spezifikation wurde ein neuer Bytecode eingeführt, der eine abgeschwächte Typisierung erlaubt, aber bezogen auf Primitive- und Objekttypen ist der Bytecode nach wie vor streng typisiert. Mit der Bytecode-Instruktion invokedynamic steht ein Methodenaufruf zur Verfügung, der eine dynamische Verknüpfung vom Aufrufer (Call Site) zum Empfänger (Call Receiver) erlaubt. Damit kann eine Klasse, die gerade einen Methodenaufruf ausführt, mit der Klasse und Methode verknüpft werden, die diesen Aufruf zur Laufzeit erhält. Die anderen JVM-Bytecode-Instruktionen zum Methodenaufruf, wie invokevirtual, bekommen die Target-Type-Information direkt in die kompilierte Klassendatei hineinkodiert. Ähnlich wie beim Funktions-Pointer, erhält man mit invokedynamic die Möglichkeit Aufrufe abzusetzen, ohne die üblichen Java-Sprachprüfungen durchführen zu müssen, komplett benutzerdefinierte Verknüpfungen herzustellen und den direkten Austausch von Methoden-Call-Targets. Die Verknüpfung von Call Site und Call Receiver erfolgt nicht durch javac, sondern wird dem Entwickler abverlangt, wenn er den Bytecode für dynamische Sprachen erzeugt. In Abbildung 3 wird das Konzept von Call Site java.lang.invoke.CallSite dargestellt. Über invokedynamic wird die Bootstrap-Methode aufgerufen (Schritt 1) und gibt die CallSite zurück return new CallSite(); (Schritt 2), mit genau einem invokedynamic-Aufruf pro Call Site. Die CallSite enthält den MethodHandle mit dem Aufruf der tatsächlichen Target-Methodenimplementierung (Schritt 3), und die Call Site ist komplett mit dem invokedynamic-Bytecode verknüpft (Schritt 4). Der MethodHandle ist das Target, das auch über getTarget/setTargetverändert werden kann (Listing 1).

 

Abb. 3: invokedynamic-Bytecode und „MethodHandle“

 20: invokedynamic #97,0
// InvokeDynamic #0:”func”:(Ljava/lang/Object; 
     Ljava/lang/Object;)V

public static CallSite bootstrap(
     final MethodHandles.Lookup lookup,
     final String name,
     final MethodType type,
     Object… callsiteSpecificArgs) {

     // look up the target MethodHandle in some way
     MethodHandle target = f(
          name,
          callSiteSpecificArgs);
          // do something
     // instantiate the callsite
     CallSite cs = new MutableCallSite(target);
     // do something

     return cs;
}

Das Konzept vom MethodHandle (java.lang.invoke.MethodHandle) ist im Listing 2 dargestellt und beschreibt einen eigenen „Funktions-Pointer“. Die Programmierlogik könnte mit Guards c = if (guard) a(); else b(); abgesichert sein, sowie Parametertransformationen und -Bindings beinhalten (Listing 3). Ein SwitchPoint (java.lang.invoke.SwitchPoint) enthält eine Funktion zweier MethodHandles a und b sowie eine Invalidierung durch Überschreiben der CallSite a nach b. Ein SwitchPoint ist ein Objekt, um Zustandsübergänge anderen Threads bekannt zu machen. Betrachtet man die Performance von invokedynamic in der JVM, so kennt diese das CallSite Target und kann Inlining mit üblichen adaptiven Laufzeitannahmen verwenden, wie beispielsweise „Guard benutzen“, und kommt damit ohne komplizierte Mechanismen aus. Theoretisch sollte sich daraus eine passable Performance herleiten lassen; doch wechseln die CallSite Targets im Code zu oft, wird der Programmierer durch die JVM-Code-Deoptimierung wieder abgestraft.

MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle mh = lookup.findVirtual(String.class, "replace", mt);

String s = (String)mh.invokeExact("daddy", 'd', 'n');
assert "nanny".equals(s) : s;

 

MethodHandle add =
     MethodHandles.guardWithTest(
          isInteger,
          addInt
          addDouble);

SwitchPoint sp = new SwitchPoint();
MethodHandle add = sp.guardWithTest(
     addInt,
     addDouble);

// do something

if (notInts()) {
     sp.invalidate();
}

[ header = Seite 3: Implementierung dynamischer Sprachen auf der JVM ]

Implementierung dynamischer Sprachen auf der JVM

Eine Vielzahl von dynamischen Sprachen, wie beispielsweise Jython, JRuby, Groovy, Scala, Kotlin und JavaScript, können die JVM als Ablaufumgebung benutzen, weil sie die Sprachentwickler für die JVM implementiert haben. Die JVM kennt nur Bytecode, keine Sprachsyntax, und der Bytecode ist komplett plattformunabhängig. Damit müssen nur die Call Sites umgeschrieben werden und die bestehenden Annahmen geändert werden, aber ungeachtet dessen, liegt die große Schwierigkeit bei den JVM-Typen. Das Problem mit sich ändernden Annahmen während der Laufzeit kann von größerem Umfang sein als die üblichen Problemstellungen in einem Java-Programm, bei dem z. B. ein Feld gelöscht wurde. Dann müssen alle betroffenen Stellen geändert werden, bei denen die Annahme zutrifft, dass das Objektlayout zu viele Felder besitzt. Im Gegensatz dazu würde man die Berechnungsergebnisse auf Grundlage mathematischer Funktionalität, wie Math.cos() und Math.sin(), auf keinen Fall ändern und immer einen konstanten Wert zurückliefern, obwohl sie in JavaScript änderbar sind. Auch wenn dies niemand ändert, muss es doch abgeprüft werden. Setzt man die strenge Typisierung von Java in Bezug mit schwacher Typisierung anderer Sprachen und betrachtet das Problem von schwacher Typisierung genauer und nimmt man unten stehende Java-Methode und schaut sich über javap –c den Bytecode an, stellt man fest, dass in Java int-Typen zur Laufzeit bekannt sind (siehe Opcodes und Maschinen-Stack). Soll ein Type Double zur Addition kompiliert werden, ist dies nicht zulässig.

int sum(int a, int b) {
  return a + b;
}
Method int sum(int,int)
0   iload_1        // Push value of local variable 1 (a)
1   iload_2        // Push value of local variable 2 (b)
2   iadd           // Add; leave int result on operand stack
3   ireturn        // Return int result
iload_1       // stack: a
iload_2       // stack: a, b
iadd          // stack: (a+b)
ireturn       // stack:

Betrachtet man stattdessen eine JavaScript Funktion, so ist nicht bekannt, ob a und b tatsächlich addiert werden können, da eine Typdefinition fehlt und der JavaScript-Operator + auch zur Verknüpfung von Zeichenketten verwendet werden kann. Bei String-Verarbeitung wird dementsprechend keine Berechnung durchgeführt. Die Definition vom Additonsoperator (+) ist in ECMA 262 so beschrieben, dass + zur Konkatenation von Strings oder zur numerischen Addition verwendet wird.

function sum(a, b) {
  return a + b;
}
??? 
??? 
??? 
??? 

In JavaScript können a und b als Integer behandelt werden, die in 32 Bit passen, aber die Addition kann zum Überlauf führen und das Ergebnis in ein Long oder als Double gespeichert werden. Deshalb ist für die JVM eine JavaScript-„Number“ nur sehr schwer greifbar, was auch für andere dynamische Sprachen wie JRuby gilt. Damit ist bei schwacher Typisierung die Typherleitung zur Laufzeit auch zu schwach.
Betrachtet man das Axiom vom adaptiven Laufzeitverhalten, so gleicht es oftmals einem Spiel, bei dem der schlechtere und langsamere Fall wahrscheinlich nicht eintritt. Falls er doch eintreten sollte, wird die Strafe zwar in Kauf genommen, aber erst später und nicht zum aktuellen Zeitpunkt. Mithilfe von Pseudocode wird der Gedankengang veranschaulicht, mit dem Schwerpunkt auf der Typspezialisierung, ohne dabei die Mechanismen von Java 7 und Java 8 zu verwenden:

function sum(a, b) {
     try {
          int sum = (Integer)a + (Integer)b;
          checkIntOverflow(a, b, sum);
          return sum;
     } catch (OverFlowException | ClassCastException e) {
          return sumDoubles(a, b);
     }
}

Allgemeiner ausgedrückt sieht es so aus:

final MethodHandle sumHandle = MethodHandles.guardWithTest(
     intsAndNotOverflow,
     sumInts,
     sumDoubles);

function sum(a, b) {
     return sumHandle(a, b);
}

Es können auch andere Mechanismen als Guards verwendet werden, beispielsweise durch Umschreiben vom Target MethodHandle im Falle einer ClassCastException oder durch SwitchPoints. Der Ansatz kann auch auf Strings und andere Objekte erweitert werden. Jedoch sollten die Compile-Time-Typen verwendet werden, falls sie verfügbar sind. In der Betrachtung werden vorerst keine Integer-Overflows berücksichtigt, die Konvertierung von Primitive Number zu Object wird als übliches Szenario angesehen und Runtime-Analyse und Invalidierung mit den statischen Typen vom JavaScript-Compiler kombiniert. Durch Spezialisierung der sum function für die Call Site werden Doubles schneller ausgeführt als semantisch äquivalente Objekte, und dies ist mit nur vier Bytecodes kürzer, ohne die Runtime-Aufrufe:

// specialized double sum
sum(DD)D:
dload_1
dload_2
dadd
dreturn

Nachdem in dynamischen Sprachen einiges passieren kann, stellt sich die Frage: Was geschieht, wenn die Funktion „sum“ zwischen den Call-Site-Ausführungen wie folgt überschrieben wird?

sum = function(a, b) {
return a + 'string' + b;
}

Expliziten Bytecode braucht man dabei nicht zu verwenden, sondern man nimmt einen Switch Point und generiert einen Revert Stub, um den Rückweg abzusichern. Die Call Site zeigt dann auf den Revert Stub und nicht auf die Spezialisierung mit Double. Keiner der Revert Stubs muss als tatsächlicher, expliziter Bytecode generiert werden, sondern die MethodHandle Combinators reichen dafür aus.

sum(DD)D:
dload_1
dload_2
dadd
dreturn
sum_revert(DD)D: //hope this doesn’t happen
dload_1
invokestatic JSRuntime.toObject(D)
dload_2
invokestatic JSRuntime.toObject(D)
invokedynamic sum(OO)O
invokestatic JSRuntime.toNumber(O)
dreturn

Das Ergebnis sieht dann wie folgt aus:

ldc 4711.17				ldc 4711.17
dstore 1					dstore 1
ldc 17.4711				ldc 17.4711
dstore 2					dstore 2
dload 1					dload 1
invoke JSRuntime.toObject(O)
dload 2					dload 2
invoke JSRuntime.toObject(O	//likely inlined:
invokedynamic sum(OO)O		invokedynamic sum(DD)D
invoke JSRuntime.toDouble(O)
dload 3					dload 3
dmul					        dmul
dstore 3					dstore 3

[ header = Seite 4: Einsatzgebiet von Nashorn ]

Einsatzgebiet von Nashorn

Nashorn ist ein vortrefflicher Anwendungsfall für invokedynamic. Damit existiert eine rein in Java geschriebene und invokedynamic-basierte Implementierung für dynamische Sprachen auf der JVM, die schneller sein soll als andere Implementierungen ohne invokedynamic. Dieser Ansatz ist mit der JVM-Implementierung von JRuby direkt vergleichbar. Für Nashorn wurde JavaScript ausgewählt, einerseits wegen seiner großen Verbreitung und dem hohen Potenzial, neue Anwendungen mit JavaScript zu schreiben. Andererseits, um die langsame Rhino-Engine durch Nashorn im JDK 8 komplett zu ersetzen. JSR 223 mit javax.script ist das zentrale API direkt auf der JVM, über das Java/JavaScript und umgekehrt JavaScript/Java automatisch integriert wird. Beispielhaft für die direkte Verwendung von JSR 223 mit Nashorn ist der von Jim Laskey in 84 Zeilen geschriebene Nashorn-HTTP-Server (Listing 4). Weitere Beispiele sind im Nashorn-Blog aufgeführt (https://blogs.oracle.com/nashorn/), u. a. die Verwendung von Lambda-Ausdrücken mit Nashorn.

#!/usr/bin/jjs -scripting
#

var Thread            = java.lang.Thread;
var ServerSocket      = java.net.ServerSocket;
var PrintWriter       = java.io.PrintWriter;
var InputStreamReader = java.io.InputStreamReader;
var BufferedReader    = java.io.BufferedReader;
var FileInputStream   = java.io.FileInputStream;
var ByteArray         = Java.type("byte[]");

var PORT = 8080;
var CRLF = "rn";
var FOUROHFOUR = <<<EOD;
<HTML>
    <HEAD>
        <TITLE>404 Not Found</TITLE>
    </HEAD>
    <BODY>
        <P>404 Not Found</P>
    </BODY>
</HTML>
EOD

var serverSocket = new ServerSocket(PORT);

while (true) {
    var socket = serverSocket.accept();
    
    try {
        var thread = new Thread(function() { httpRequestHandler(socket); });
        thread.start();
        Thread.sleep(100);
    } catch (e) {
        print(e);
    }
}

function httpRequestHandler(socket) {
    var out       = socket.getOutputStream();
    var output    = new PrintWriter(out);
    var inReader  = new InputStreamReader(socket.getInputStream(), 'utf-8');
    var bufReader = new BufferedReader(inReader);
    
    var lines = readLines(bufReader);
    
    if (lines.length > 0) {
        var header = lines[0].split(/bs+/);

        if (header[0] == "GET") {
            var URI = header[1].split(/?/);
            var path = String("./serverpages" + URI[0]);
    
            try {
                if (path.endsWith(".jjsp")) {
                    var body = load(path);
                    if (!body) throw "JJSP failed";
                    respond(output, "HTTP/1.0 200 OK", "text/html", body);
                } else {
                    sendFile(output, out, path);
                }
            } catch (e) {
                respond(output, "HTTP/1.0 404 Not Found", "text/html", FOUROHFOUR);
            }
        }
    }
    
    output.flush();
    bufReader.close();
    socket.close();
}

function respond(output, status, type, body) {
    sendBytes(output, status + CRLF);
    sendBytes(output, "Server: Simple Nashorn HTTP Server" + CRLF);
    sendBytes(output, "Content-type: ${type}" + CRLF);
    sendBytes(output, "Content-Length: ${body.length}" + CRLF);
    sendBytes(output, CRLF);
    sendBytes(output, body);
}

function contentType(path) {
    if (path.endsWith(".htm") ||
        path.endsWith(".html")) {
      return "text/html";
    } else if (path.endsWith(".txt")) {
      return "text/text";
    } else if (path.endsWith(".jpg") ||
               path.endsWith(".jpeg")) {
      return "image/jpeg";
    } else if (path.endsWith(".gif")) {
      return "image/gif";
    } else {
      return "application/octet-stream";
    }
}

function readLines(bufReader) {
    var lines = [];
    
    try {
        var line;
        while (line = bufReader.readLine()) {
            lines.push(line);
        }
    } catch (e) {
    }
    
    return lines;
}

function sendBytes(output, line) {
    output.write(String(line));
}

function sendFile(output, out, path) {
    var file = new FileInputStream(path);

    var type = contentType(path);
    sendBytes(output, "HTTP/1.0 200 OK" + CRLF);
    sendBytes(output, "Server: Simple Nashorn HTTP Server" + CRLF);
    sendBytes(output, "Content-type: ${contentType(path)}" + CRLF);
    sendBytes(output, "Content-Length: ${file.available()}" + CRLF);
    sendBytes(output, CRLF);
    output.flush();
    
    var buffer = new ByteArray(1024);
    var bytes = 0;
    
    while ((bytes = file.read(buffer)) != -1) {
        out.write(buffer, 0, bytes);
    }
}

Das Einsatzgebiet von Nashorn erstreckt sich über Online-Interpreter in JavaScript mit REPL (Read Eval Print Loop), vgl. repl.it für native Mozilla Firefox JavaScript, Shell-Skripte, Build-Skripte mit dem ant-Eintrag <script language=“javascript“>, bis hin zu serverseitigen JavaScript-Frameworks mit Node.jar. Mit dem eigenständigen Projekt Node.jar, existiert eine Nashorn-basierte Implementierung des Node.js-Event-API, die mit der Node-Test-Suite auf vollständige Kompatibilität getestet wurde, aber ohne ein bisher veröffentlichtes Releasedatum (Stand: Juli 2013, Anm. d. Red.). Dabei können das Node.js-Event-Modell, das Modulsystem und die Java-Plattform-APIs verwendet werden. Node.jar verbindet über ein Mapping das Node-API mit korrespondierender Java-Funktionalität (Tabelle 1) und enthält die asynchrone I/O-Implementierung vom Grizzly-Multiprotokoll-Framework.
Die Vorteile beim Betrieb von Node-Anwendungen auf der JVM liegen in der Wiederverwendbarkeit existierender Java-Bibliotheken und Laufzeitumgebungen, der Nutzung mehrerer Java-Threads- und Multi-Core-Prozessoren, im Gegensatz zu einem Node-Port, der immer Single-Threaded und ereignisgesteuert abläuft. Zusätzlich bekommt man bei Java das Java-Security-Modell dazu, und mit Nashorn erhält man alle vom JDK 8 unterstützten Plattformen, auch die für Java SE Embedded.

Node.js Java-Funktionalität
buffer Grizzly HeapBuffer (java.nio.ByteBuffer)
child_process java.lang.ProcessBuilder
dns java.net.InetAddress, java.net.IDN
fs java.nio.File
http / https Grizzly
net java.net.Socket, java.net.ServerSocket, java.nio.channels.Selector
os java.lang.System, java.lang.Runtime
stream Grizzly nio Stream

Tabelle 1: Mapping der Node.js-Module nach Java

Bei der Verwendung von Node.jar und Java-Persistierung gibt es mehrere Möglichkeiten: Erstens über einen JPA-Wrapper (Abb. 4), durch XML und annotierte Java Klassen erfolgt der Java-EE-Zugriff von serverseitigem JavaScript. Zweitens die Variante Dynamic-JPA-Wrapper, mit XML-Konfiguration und dem Java-EE-Zugriff von serverseitigem JavaScript, ohne Java-Klassen zu benutzen. Die dritte Variante JavaScript-JPA wird über JavaScript/JSON konfiguriert, einer Java-SE-JPA-orientierten JavaScript-Persistenz mit JSON-Konfiguration, und die vierte Variante JavaScript-Datenbank-JPA benutzt JavaScript-JPA über ein Datenbankschema. Die Java-SE-JavaScript-Persistenz verwendet JPA mit den Mappings vom Datenbankschema. Für die Varianten drei und vier gilt Abbildung 5.

Abb. 4: Annotierte Java-Klassen mit XML-Konfigurator

Abb. 5: JavaScript-/JSON-Konfiguration

[ header = Seite 5: Make Nashorn]

Make Nashorn

Der Download von Nashorn im Bundle mit JDK 8 kann über java.net durchgeführt werden. Alternativ kann der Nashorn-Forest-Build mit der Umgebungsvariablen JAVA_HOME auf dem entsprechenden JDK-8-Installationsverzeichnis auch selbst gemacht werden. Unter dem Verzeichnis make kann die ca. 1,5 MB große Datei nashorn.jar erstellt werden.

hg clone http://hg.openjdk.java.net/nashorn/jdk8/nashorn nashorn

cd nashorn~jdk8/nashorn/make
ant clean jar

Um Nashorn verwenden zu können, braucht man das Kommando jjs mit dem Interpreter vom JDK 8 und man erhält die REPL.

cd nashorn~jdk8/nashorn
sh bin/jjs <your .js file>

Für Nashorn gibt es auch eine JavaFX-Integration, die in der Kommandozeile mit dem Aufruf jjs -fx fxscript.js verwendet wird. Der Einstellparameter -fx für jjs macht das Bootstrapping für Skripte, die javafx.application.Application verwenden.
Nashorn unterstützt das javax.script-API. Damit ist es auch möglich, die Datei nashorn.jar in den Classpath aufzunehmen und die Aufrufe an die Nashorn-Script-Engine über den javax.script.ScriptEngineManager abzusetzen. Die Dokumentation ist unter Nashorn JavaDoc. Sie kann wie folgt erzeugt werden und steht dann unter dist/javadoc/index.html zur Verfügung.

cd nashorn~jdk8/nashorn/make
ant javadoc

Ausblick

Das neue Projekt Nashorn in JDK 8 ist ein großer Schritt, die JVM attraktiver zu machen, gepaart mit dem langfristigen Ziel, die JVM in eine polyglotte Laufzeitumgebung zu überführen, wie im OpenJDK-Projekt Multi-Language VM (MLVM oder auch Da Vinci Machine Project) beschrieben. MLVM erweitert die JVM mit der notwendigen Architektur und Leistungsfähigkeit für andere Sprachen als Java und speziell als effiziente Ablaufumgebung für dynamisch typisierte Sprachen. Der Schwerpunkt liegt auf der Fertigstellung einer existierenden Bytecode- und Ablaufarchitektur zur allgemeinen Erweiterung, anstatt eine neue Funktionalität nur für eine einzige Sprache zu erstellen oder gar ein neues Ablaufmodell zu erzeugen. Neue Sprachen auf der JVM sollen wie Java von der leistungsstarken und ausgereiften Ablaufumgebung profitieren. Stolpersteine, die die Entwickler neuer Sprachen stören, sollen entfernt werden. Allerdings soll übermäßiger Arbeitseifer an nicht erwiesener Funktionalität oder an Nischensprachen nicht im Vordergrund stehen. Unterprojekte mit erhöhter Aktivität sind beispielsweise: Dynamic Invocation, Continuations und Stack Introspection, Tail Calls und Tail Recursion sowie Interface Injection. Dazu kommt eine Vielzahl von Unterprojekten mit niedriger Priorität. Die im MLVM Incubator befindlichen Unterprojekte Tail-Call-Optimierung und Interface Injection sind hilfreich für Scala. Besonders wünschenswert wäre eine stärkere Beteiligung dieser Entwicklergemeinschaft im Projekt MLVM. Die Gestaltung einer künftigen VM beinhaltet einen Open-Source-Inkubator für aufkommende Merkmale der JVM und sie enthält Codefragmente und Patches (Tabelle 2). Die Migration ins OpenJDK ist erforderlich sowie eine Standardisierung und ein Feature-Release-Plan. Die aus dem Projekt MLVM gewonnenen Forschungsergebnisse sollen dazu dienen, existierenden Bytecode zu verwenden, um die Performancevorteile von Nashorn im JDK 9 zu vergrößern und den HotSpot-Codegenerator für eine verbesserte Unterstützung dynamischer Sprachen anzupassen.

MLVM-Patches Beschreibung
meth method handles implementation
indy invokedynamic
coro light weight coroutines
inti interface injection
tailc hard tail call optimization
tuple integrating tuple types
hotswap online general class schema updates
anonk anonymous classes; light weight bytecode loading

Tabelle 2: Da Vinci Machine Patches

Fazit

Nashorn ist mehr als nur ein Proof of Concept für invokedynamic, es ist eine leistungsstarke und standardkonforme JavaScript-Engine im JDK 8, die auch weitere Versionen von JavaScript und ECMAScript 6 unterstützen wird. Künftige Arbeitspakete umfassen eine Leistungssteigerung innerhalb von Nashorn und der JVM, sodass die schnelle Verarbeitung von JavaScript mit Nashorn auf der JVM nicht mehr in Frage gestellt wird. Mit dem in Nashorn neu implementierten Meta-Objekt-Protokoll werden die JavaScript-Aufrufe von Java-APIs vereinfacht. Dies ermöglicht die nahtlose Interaktion von JavaScript und Java. Denkbar ist auch eine Alternative am Browser, bei der die Entwickler wählen können, ob sie die Google V8 JavaScript Engine mit WebKit oder auch dort mit Nashorn verwenden möchten. Es bleibt offen, wie stark die Verbreitung von Anwendungen mit JavaScript am Browser und auf dem Server tatsächlich zunehmen wird, aber Nashorn ermöglicht es den Infrastrukturanbietern, die Verarbeitung von JavaScript und Java auf eine zentrale JVM zu konsolidieren. Die JVM wird dadurch attraktiver und bietet neben Memory-Management, Codeoptimierung, Scripting für die Java-Plattform (JSR 223) auch viele nützliche JVM-Managementwerkzeuge mit Debugging- und Profiling-Fähigkeiten an. Dies ist ein grundlegender Meilenstein auf dem Weg zur polyglotten VM, eine zuverlässige und skalierbare Infrastruktur für neue Sprachen anzubieten.

Geschrieben von
Marcus Lagergren
Marcus Lagergren
Marcus Lagergren hat einen MSc in Computer Science vom Royal Institute of Technology in Stockholm. Er ist ein Gründungsmitglied von Appeal Virtual Machines, der Firma hinter der JRockit JVM. Lagergren war Team Lead und Architekt der JRockit Code Generators und über die Jahre in fast alle Aspekte der JVM involviert. Anschließend wechselte er zu Oracle für die Arbeit an Virtualisierungstechnologien, und seit 2011 ist er Mitglied des Java-Language-Teams bei Oracle und eruiert dynamische Sprachen auf der JVM.
Wolfgang Weigend
Wolfgang Weigend
Wolfgang Weigend arbeitet als Sen. Leitender Systemberater bei der Oracle Deutschland B.V. & Co. KG. Er beschäftigt sich mit Java-Technologie und -Architektur für unternehmensweite Anwendungsentwicklung.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: