Java8:使用新JS解釋器Nashorn編譯Lambda表達式

在最近的一篇文章中,我瞭解了一下Java8和Scala是如何實現 Lambda 表達式的。正如我們所知道的,Java8不僅對javac編輯器做了很大改進,它還加入了一個全新的項目---Nashorn。

這個新的解釋器將會代替Java現有的Rhino解釋器。據說它執行JavaScript的速度非常之快,就像世界上最快的跑車 V8s,所以,我覺得現在很有必要打開Nashorn源碼,看看它是如何編譯 Lambda 表達式的(着重於Java 和 Scala的對比)。

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

代碼如下:

criptEngineManager 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代理來獲得並保存生成的字節碼,其實就是一個簡單的javap反編譯器了。

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

讀取字節碼

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(); }

堆棧的調用完整情況請看這裏

原文:http://www.takipiblog.com/2014/02/10/java-8-compiling-lambda-expressions-in-the-new-nashorn-js-engine/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章