1. 程式人生 > >Java8之使用新JS直譯器Nashorn編譯Lambda表示式

Java8之使用新JS直譯器Nashorn編譯Lambda表示式

在最近的一篇文章中,我瞭解了一下Java8和Scala是如何實現 Lambda 表示式的。正如我們所知道的,Java8不僅對javac編輯器做了很大改進,它還加入了一個全新的專案—Nashorn。這個新的直譯器將會代替Java現有的Rhino直譯器。據說它執行JavaScript的速度非常之快,就像世界上最快的跑車 V8s,所以,我覺得現在很有必要開啟Nashorn原始碼,看看它是如何編譯 Lambda 表示式的(著重於Java 和 Scala的對比)。

我們使用Java和Scala測試的 lambda表示式是非常相似的。

程式碼如下:

jcriptEngineManager 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直譯器則不同,Nashorn 編譯後的資料都在記憶體中,然後把位元組碼支援傳給JVM。我寫了一個簡單的Java代理來獲得並儲存生成的位元組碼,其實就是一個簡單的javap反編譯器了。

我看到Java8編譯器使用了 invokeDynamic指令感到特別激動, invokeDynamic指令是在Java7中被引用的,目的是呼叫 Lambda函式。現在基於 Nashorn的工作都已經做完了,繼續往下看。

讀取位元組碼

invokeDynamic 指令:這個指令和我們整篇文章密切相關。Java 7 引入invokeDynamic 指令的目的是為了讓開發人員可以自己去編寫動態語言,決定在執行時如何連結程式碼,

對於像Java和Scala這樣的靜態語言來說,編譯器在編譯的時候就決定了哪一個方法將會被呼叫(而Java的多型性是通過JVM的一些的工具實現的),執行時的連結是通過 ClassLoaders載入類來完成的,甚至方法過載都是在編譯時期完成的。

動態連結 VS 靜態連結:很不幸,對於動態語言來說,靜態解析也許是不可能的(JS就是一個很好的例子),當我們在Java語言中執行 obj.foo() 方法時,obj物件的類中也許有foo()方法,也許沒有,而在一個類似JS的語言中,則取決於執行時obj實際物件的引用—靜態編譯器的噩夢。編譯時連結在這個時候根本不起作用,不過 invokeDynamic指令可以做到。

InvokeDynamic 指令可以在執行時推遲返回這個語言的開發者的連結,所以它們能夠根據自己的語義引導JVM呼叫哪一個方法,這是一個雙贏的方案。JVM可以獲得一個實際的連結方法,並進行優化,執行,而且語言開發者可以控制自己的解析方案。在Takipi這個網站中我們必須努力去支援動態連結。

Nashorn直譯器如何連結:Nashorn很好的利用了這一點。讓我們看一看一個例子來理解Nashorn是如何工作的。程式碼的作用是用來檢索JS陣列類的值:

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

Nashorn需要JVM在執行時傳遞一個String型別引數,並返回一個方法,這個方法接受一個Object型別的引數,同時返回一個Object型別的物件。只要JVM獲得這個方法的一個控制代碼(handle),就會連結。

這個方法負責返回一個控制代碼(就是一個載入程式的方法–bootstrap method),在.class檔案中的一個特殊部分被指定,持有一系列的引導方法。你看到的0是表的索引,JVM呼叫方法獲得方法的控制代碼,JVM就是用這個控制代碼進行連結的。

我認為Nashorn專案開發團隊做了一件很爽的事情,那就是不再需要他們自己編寫解析和連結程式碼的庫了,而是集成了dynalink專案,這個開源專案是為了在一個統一的平臺上將動態語言連結成程式碼。這就是為什麼在每一個String之前都有一個”dyn:”字首的原因了。

實際的工作流

既然我們已經完成了Nashorn所使用的方法,下面就讓我們看一看實際流。為了簡潔,我去掉了一些不重要的程式碼。整個程式碼可以在這裡下載。 1、這段兒程式碼作用是載入JS陣列函式對映到指令碼中

//載入JS陣列(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;

//載入map方法(load the map method)
invokedynamic 0 "dyn:getProp|getElem|getMethod:map":(Ljava/lang/Object;)Ljava/lang/Object;

//set到本地(set it to the map local)
invokedynamic 0 #0:"dyn:setProp|setElem:map":(Ljava/lang/Object;Ljava/lang/Object;)V

2、分配names 陣列

//把names陣列分成JS物件(allocate the names array as a JS object)
invokestatic jdk/nashorn/internal/objects/Global.allocate:([Ljava/lang/Object;)Ljdk/nashorn/internal/objects/NativeArray;

//將物件放到names中(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、找到並載入Lambda 函式

//為在執行時被Nashorn編譯的指令碼載入常量(load the constants field for this script compiled and filled at runtime by Nashorn)
getstatic constants

//將2放到棧頂,Nashorn將會把控制代碼放到lambda程式碼中(refer to the 2nd entry, where Nashorn will place a handle to the lambda code)
iconst_2

//從常量陣列中獲取它(get it from the constants array)
aaload

//檢察它是否是一個JS函式物件(ensure it’s a JS function object)
checkcast class jdk/nashorn/internal/runtime/RecompilableScriptFunctionData

4、通過傳入引數names和Lambda呼叫map函式,把結果存放到a中

//呼叫map函式,把names和棧中返回的Lambda函式當做引數傳入(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;

//把返回結果存放到a中(put the result in a)
invokedynamic 0 #0:"dyn:setProp|setElem:a":(Ljava/lang/Object;Ljava/lang/Object;)V

5、找到print函式,並用a呼叫它

//載入print函式(load the print function)
invokedynamic 0 #0:"dyn:getMethod|getProp|getElem:print":(Ljava/lang/Object;)Ljava/lang/Object;

//載入a(load a)
invokedynamic 0 #0:"dyn:getProp|getElem|getMethod:a":(Ljava/lang/Object;)Ljava/lang/Object;

//呼叫print函式(call print on it)
invokedynamic 0 #2:"dyn:call":(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

lambda函式和指令碼一樣被編譯並放到相同的類中,作為一個private方法。這個和Java8中lambdas表示式是非常相似的。程式碼非常簡單,我們載入String,並找到lengh()方法,然後呼叫它。

//載入引數名稱(Load the name argument (var #1))
aload_1

//找到length()方法(find its length() function)
invokedynamic 0 "dyn:getMethod|getProp|getElem:length":(Ljava/lang/Object;)Ljava/lang/Object;

//呼叫length()(call length)
invokedynamic 0 "dyn:call":(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

//返回結果(return the result)
areturn

獎勵環節-最後的位元組碼

到目前為止,我們所完成的程式碼不能在JVM執行時執行。要記住,每一個invokeDynamic 指令將會被處理成一個物理位元組碼方法,然後由JVM將其編譯成機器語言並執行。 為了看到JVM執行的真正位元組碼,我使用了一個技巧,我在類中使用一個簡單的方法wrap(String s)去呼叫length()方法。這就需要我放一個斷點,這樣就可以看到JVM執行時的堆疊情況。

程式碼如下: js += “var a = map.call(names, function(name) { return Java.type(“LambdaTest”).wrap(name.length()) })”;

這是wrap方法: public static int wrap(String s) { return s.length(); }

堆疊的呼叫完整情況請看這裡