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(); }
堆栈的调用完整情况请看 这里。
(全文完)如果您喜欢此文请点赞,分享,评论。
- 原创文章转载请注明出处: Java8之使用新JS解释器Nashorn编译Lambda表达式
- 小额赞助本站:: 我要赞助
您可能感兴趣的文章
- 2014 年 4 月 11 日 Java8初体验(二)Stream语法详解 (6)
- 2014 年 4 月 11 日 Java8初体验(一)lambda表达式语法 (0)
- 2014 年 3 月 16 日 Java8简单的本地缓存实现 (4)
- 2014 年 4 月 11 日 Java8初体验系列文章 (0)
- 2014 年 3 月 15 日 话说模式匹配(7) 一个构造器模式的例子(by case class) (0)
- 2014 年 3 月 15 日 话说模式匹配(6) case类的细节 (0)
- 2013 年 8 月 9 日 讨喜的隔离可变性(四)收发消息 (0)
- 2013 年 3 月 21 日 话说模式匹配(2) scala里是怎么实现的? (1)
- 2013 年 8 月 9 日 讨喜的隔离可变性(三)创建角色 (2)
- 2013 年 8 月 9 日 讨喜的隔离可变性(十一)调和类型化角色 (0)
- 2013 年 7 月 19 日 软件事务内存导论(五)创建嵌套事务 (3)
- 2013 年 8 月 9 日 讨喜的隔离可变性(七)使用类型化角色 (0)
- 2013 年 8 月 9 日 讨喜的隔离可变性(十)使用Transactor (0)
- 2013 年 8 月 9 日 讨喜的隔离可变性(八)类型化角色和Murmurs (0)
- 2013 年 3 月 21 日 话说模式匹配(1) 什么是模式? (0)