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

堆栈的调用完整情况请看 这里

Tagged:

Comments are closed.