Статьи

Java 8: Компиляция лямбда-выражений в новом движке Nashorn JS

В недавнем посте я рассмотрел, как в Java 8 и Scala реализованы лямбда-выражения. Как мы знаем, Java 8 не только вносит улучшения в компилятор javac, но и представляет совершенно новый — Nashorn.

Этот новый движок предназначен для замены существующего в Java интерпретатора JavaScript Rhino. Это должно вывести JVM на передний план, когда дело доходит до выполнения JavaScript на скорости, прямо там с V8 мира (надеюсь, мы наконец-то пройдем эту машину, чтобы ковровить штуку :)) Итак, я думал, что это будет хорошее время, чтобы познакомить Nashorn с миксом, заглянув под капот и посмотрев, как он компилирует лямбда-выражения (особенно по сравнению с Java и Scala).

Лямбда-выражение, которое мы рассмотрим, похоже на то, которое мы тестировали на Java и Scala.

Вот код:

01
02
03
04
05
06
07
08
09
10
11
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
 
String js;
 
js = "var map = Array.prototype.map \n";
js += "var names = [\"john\", \"jerry\", \"bob\"]\n";
js += "var a = map.call(names, function(name) { return name.length() })\n";
js += "print(a)";
 
engine.eval(js);

Кажется, ты невиновен. Но просто подожди и посмотри …

Получение к байт-коду

Наша первая задача — получить действительный байт-код, который видит JVM. В отличие от Java и Scala, чьи компиляторы являются постоянными (т.е. генерируют файлы .class / jar на диск), Nashorn компилирует все в памяти и передает байт-код непосредственно в JVM. К счастью, у нас есть Java-агенты, которые могут нам помочь. Я написал простой Java-агент для захвата и сохранения полученного байт-кода. С этого момента это простой javap для печати кода.

Если вы помните, я был очень рад видеть, как новый компилятор Java 8 использует инструкцию invokeDynamic, введенную в Java 7, для ссылки на код функции Lambda. Ну, с Nashorn они действительно пошли на гонки с этим. Все теперь полностью основано на этом. Посмотрите ниже.

Чтение байт-кода

invokeDynamic . Точно так, что мы все на одной странице, инструкция invokeDynamic была добавлена ​​в Java 7, чтобы позволить людям, пишущим свои собственные динамические языки, решать во время выполнения, как связывать код.

Для статических языков, таких как Java и Scala, компилятор решает во время компиляции, какой метод будет вызван (с некоторой помощью из среды выполнения JVM для полиморфизма). Связывание во время выполнения осуществляется через стандартные ClassLoaders для поиска класса. Даже такие вещи, как разрешение перегрузки метода выполняются во время компиляции.

Динамическая и статическая связь . К сожалению, для языков, которые более динамичны по своей природе (и JS является хорошим примером), статическое разрешение может оказаться невозможным. Когда мы говорим obj.foo () в Java, либо класс obj имеет метод foo (), либо его нет. В таком языке, как JS, это будет зависеть от фактического объекта, на который ссылается obj во время выполнения — кошмарный сценарий для статического компилятора. Подход компиляции во время компиляции в этом случае просто не работает. Но invokeDynamic делает.

InvokeDynamic позволяет отложить привязку обратно к авторам языка во время выполнения, чтобы они могли указывать JVM, какой метод они хотели бы вызвать, основываясь на их собственной языковой семантике. Это беспроигрышная ситуация. JVM получает реальный метод для связи, оптимизации и выполнения, а создатели языка контролируют его разрешение. Динамическое связывание — это то, что мы должны были усердно работать, чтобы поддерживать в Такипи .

Как нашорн ссылки

Нашорн действительно эффективно использует это. Давайте посмотрим на наш пример, чтобы понять, как это работает. Вот первая инструкция invokeDynamic в лямбда-коде, которая используется для получения значения класса JS Array —

1
invokedynamic 0 "dyn:getProp|getElem|getMethod:prototype":(Ljava/lang/Object;)Ljava/lang/Object;

Nashorn просит JVM передать ей эту строку во время выполнения, и взамен она вернет дескриптор метода, который принимает Object и возвращает его. Пока JVM получает дескриптор такого метода, он может связываться.

Метод, отвечающий за возврат этого дескриптора (также известный как метод начальной загрузки), указан в специальном разделе файла .class, в котором содержится список доступных методов начальной загрузки. Значение 0, которое вы видите, является индексом в этой таблице метода, который JVM будет вызывать, чтобы получить дескриптор метода, на который она будет ссылаться.

По моему мнению, ребята из Nashorn сделали очень классную вещь, и вместо того, чтобы писать свою собственную библиотеку для разрешения и связывания кода, они продолжили работу и интегрировали dynalink , проект с открытым исходным кодом, направленный на то, чтобы помочь динамическим языкам связывать код на основе унифицированной платформы. Вот почему вы видите префикс «dyn:» в начале каждой строки.

Фактический поток

Теперь, когда мы познакомились с подходом, используемым Nashorn, давайте посмотрим на реальный поток. Я удалил некоторые инструкции для краткости. Полный код можно найти здесь .

1. Эта первая группа инструкций загружает функцию массива в скрипт.

01
02
03
04
05
06
07
08
09
10
11
//load JS array
invokedynamic 0 "dyn:getProp|getElem|getMethod:Array":(Ljava/lang/Object;)Ljava/lang/Object;
 
//load its prototype element
invokedynamic 0 "dyn:getProp|getElem|getMethod:prototype":(Ljava/lang/Object;)Ljava/lang/Object;
 
//load the map method
invokedynamic 0 "dyn:getProp|getElem|getMethod:map":(Ljava/lang/Object;)Ljava/lang/Object;
 
//set it to the map local
invokedynamic 0 #0:"dyn:setProp|setElem:map":(Ljava/lang/Object;Ljava/lang/Object;)V

2. Далее мы выделяем массив имен

1
2
3
4
5
6
7
//allocate the names array as a JS object
invokestatic jdk/nashorn/internal/objects/Global.allocate:([Ljava/lang/Object;)Ljdk/nashorn/internal/objects/NativeArray;
 
//places it into names
invokedynamic 0 #0:"dyn:setProp|setElem:names":(Ljava/lang/Object;Ljava/lang/Object;)V
 
invokedynamic 0 #0:"dyn:getProp|getElem|getMethod:names":(Ljava/lang/Object;)Ljava/lang/Object;

3. Найдите и загрузите лямбда-функцию

01
02
03
04
05
06
07
08
09
10
11
//load the constants field for this script compiled and filled at runtime by Nashorn
getstatic constants
 
//refer to the 2nd entry, where Nashorn will place a handle to the lambda code
iconst_2
 
//get it from the constants array
aaload
 
//ensure it’s a JS function object
checkcast class jdk/nashorn/internal/runtime/RecompilableScriptFunctionData

4. Позвоните в карту с именами и лямбда и поместите результат в

1
2
3
4
5
6
//call the map function, passing it names and the Lambda function from the stack
 
invokedynamic 0 #1:"dyn:call":(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljdk/nashorn/internal/runtime/ScriptFunction ;)Ljava/lang/Object;
 
//put the result in a
invokedynamic 0 #0:"dyn:setProp|setElem:a":(Ljava/lang/Object;Ljava/lang/Object;)V

5. Найдите функцию печати и вызовите ее

1
2
3
4
5
6
7
8
//load the print function
invokedynamic 0 #0:"dyn:getMethod|getProp|getElem:print":(Ljava/lang/Object;)Ljava/lang/Object;
 
//load a
invokedynamic 0 #0:"dyn:getProp|getElem|getMethod:a":(Ljava/lang/Object;)Ljava/lang/Object;
 
// call print on it
invokedynamic 0 #2:"dyn:call":(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

Сама лямбда-функция компилируется и помещается в тот же класс, что и скрипт, как частная функция. Это очень похоже на то, что мы имеем с лямбдами Java 8. Сам код прост. Мы загружаем строку, находим ее функцию длины и вызываем ее.

01
02
03
04
05
06
07
08
09
10
11
//Load the name argument (var #1)
aload_1
 
//find its length() function
invokedynamic 0 "dyn:getMethod|getProp|getElem:length":(Ljava/lang/Object;)Ljava/lang/Object;
 
//call length
invokedynamic 0 "dyn:call":(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
 
//return the result
areturn

Бонус раунд — финальный байт-код

Код, с которым мы имели дело до сих пор, не совсем то, что JVM будет выполнять во время выполнения. Помните, что каждая инструкция invokeDynamic будет преобразована в физический метод байт-кода, который JVM затем скомпилирует в машинный код и выполнит.

Чтобы увидеть фактический байт-код, который запускает JVM, я использовал простой трюк. Я обернул вызов length () простым вызовом метода Java в моем классе. Это позволило мне разместить точку останова и увидеть окончательный стек вызовов, который выполняет JVM, чтобы попасть в Lambda.

Вот код —

1
2
3
js += "var a = map.call(names, function(name) {
return Java.type("LambdaTest”).wrap(name.length())
})";

Вот упаковка —

1
2
3
4
public static int wrap(String s)
{
return s.length();
}

Теперь давайте поиграем в игру. Сколько кадров будет в этом стеке? Подумай об этом на секунду. Если ты угадал меньше <100 — ты должен мне пива. Полный стек вызовов можно найти здесь .

Причина, по которой это так, также очень интересна, и это история для целого нового поста, выходящего в будущем.