java lambda实现原理,精通lambda表达式:Java多核编程
00-1010简介1、Demo2、异常判断3、javap命令4、总结
00-1010大家都知道Java8中新加入了Lambda表达式。使用Lambda表达式可以优化很多代码,很多事情只用几行代码就可以完成。本章以Lambda为例。第一小节解释其底层执行原理,第二小节解释Lambda flow在工作中常见的姿态。
00-1010先来看一个Lambda表达式的演示,如下图:
代码比较简单,就是一个新线程打印一句话,但是对于图中的代码()-system . out . println(" lambda is run ")估计很多同学都很困惑。Java是如何识别这类代码的?
如果改成匿名内部类,就很清楚了,大家都能看懂,如下图:
那是否意味着()-system . out . println(" lambda is run ")形式的代码实际上建立了一个内部类?其实这是最简单的Lambda表达式。我们无法通过IDEA看到源代码及其底层结构。这里我们将介绍几种方法来查看它的底层实现。
00-1010我们可以在代码执行过程中主动抛出异常并打印出堆栈,堆栈会解释其运行轨迹。一般这种方法简单高效,基本上很多情况下我们都能看到隐藏的代码。我们来试试,如下图:
从异常的堆栈中,我们可以看到JVM自动为当前类创建一个内部类(在错误堆栈中表示有内部类)。在执行过程中,内部类的代码抛出异常,但是这里显示的代码是未知来源,我们也无法调试。一般来说,异常可以暴露代码执行的路径,我们可以做一个断点,再运行一次。但是对于Lambda表达式,我们只通过异常判断方法知道有异常。
00-1010javap是Java自带的查看类字节码文件的工具。安装了Java基础环境的电脑可以直接执行JavaP命令,如下图所示:
在选项中,我们主要使用命令-v -verbose,它可以完整地输出字节码文件的内容。
接下来,我们使用javap命令查看Lambda.class文件。在讲解的过程中,我们会带一些关于类文件的知识。
我们在命令窗口中找到Lambda.class的位置,执行命令:javap -verbose Lambda.class,然后你会看到一长串东西,叫做汇编指令。接下来我们就来一一解释一下(参考资料都来自Java虚拟机规范,就不一一引用了):
我们很容易在汇编指令中找到一长串以常量池开头的类型。我们称之为constant pool,官方英文名称是Run-Time Constant Pool。我们简单的把它理解为一个充满常数的表,表中包含了编译时明确的数字和单词,类、方法和字段的类型信息等等。table中的每个元素都被称为cpinfo,cpinfo由一个惟一的标识符(标签)名组成。目前,总共有以下标签类型:
贴一张我们分析过的部分图片:
图中常量池一词表示当前信息为常量池;
每行是一个cp_info,第一列的#1在常量池下标1的位置;
每行的第二列是cp_info的唯一标识符(标签)。比如Methodref对应上表中的CONSTANT_Methodref(上表中value对应10的标签),表示当前行代表方法的描述信息,比如方法的名称、参数类型、参数类型等。具体含义可以在Java虚拟机规范中找到。Methodref的截图如下:
每行第三列,如果是具体值,直接显示具体值,如果是复数值,显示cp_info的引用。例如,图中的两个红点表示位置13和14处的cp_info。13表示方法名为init,14表示方法没有返回值。组合在一起,它指示方法的名称和返回类型,这是一个。
个无参构造器;
每行的第四列,就是具体的值了。
对于比较重要的 cp_info 类型我们说明下其含义:
InvokeDynamic 表示动态的调用方法,后面我们会详细说明;Fieldref 表示字段的描述信息,如字段的名称、类型;NameAndType 是对字段和方法类型的描述;MethodHandle 方法句柄,动态调用方法的统称,在编译时我们不知道具体是那个方法,但运行时肯定会知道调用的是那个方法;MethodType 动态方法类型,只有在动态运行时才会知道其方法类型是什么。我们从上上图中标红的 3 处,发现 Ljava/lang/invoke/MethodHandles$Lookup,java/lang/invoke/LambdaMetafactory.metafactory 类似这样的代码,MethodHandles 和 LambdaMetafactory 都是 java.lang.invoke 包下面的重要方法,invoke 包主要实现了动态语言的功能,我们知道 java 语言属于静态编译语言,在编译的时候,类、方法、字段等等的类型都已经确定了,而 invoke 实现的是一种动态语言,也就是说编译的时候并不知道类、方法、字段是什么类型,只有到运行的时候才知道。
比如这行代码:Runnable runnable = () -> System.out.println(lambda is run); 在编译器编译的时候 () 这个括号编译器并不知道是干什么的,只有在运行的时候,才会知道原来这代表着的是 Runnable.run() 方法。invoke 包里面很多类,都是为了代表这些 () 的,我们称作为方法句柄( MethodHandler ),在编译的时候,编译器只知道这里是个方法句柄,并不知道实际上执行什么方法,只有在执行的时候才知道,那么问题来了,JVM 执行的时候,是如何知道 () 这个方法句柄,实际上是执行 Runnable.run() 方法的呢?
首先我们看下 simple 方法的汇编指令:
从上图中就可以看出 simple 方法中的 () -> System.out.println(lambda is run) 代码中的 (),实际上就是 Runnable.run 方法。
我们追溯到 # 2 常量池,也就是上上图中标红 1 处,InvokeDynamic 表示这里是个动态调用,调用的是两个常量池的 cp_info,位置是 #0:#37 ,我们往下找 #37 代表着是 // run:()Ljava/lang/Runnable,这里表明了在 JVM 真正执行的时候,需要动态调用 Runnable.run() 方法,从汇编指令上我们可以看出 () 实际上就是 Runnable.run(),下面我们 debug 来证明一下。
我们在上上图中 3 处发现了 LambdaMetafactory.metafactory 的字样,通过查询官方文档,得知该方法正是执行时, 链接到真正代码的关键,于是我们在 metafactory 方法中打个断点 debug 一下,如下图:
metafactory 方法入参 caller 代表实际发生动态调用的位置,invokedName 表示调用方法名称,invokedType 表示调用的多个入参和出参,samMethodType 表示具体的实现者的参数,implMethod 表示实际上的实现者,instantiatedMethodType 等同于 implMethod。
以上内容总结一下:
1:从汇编指令的 simple 方法中,我们可以看到会执行 Runnable.run 方法;
2:在实际的运行时,JVM 碰到 simple 方法的 invokedynamic 指令,会动态调用 LambdaMetafactory.metafactory 方法,执行具体的 Runnable.run 方法。
所以可以把 Lambda 表达值的具体执行归功于 invokedynamic JVM 指令,正是因为这个指令,才可以做到虽然编译时不知道要干啥,但动态运行时却能找到具体要执行的代码。
接着我们看一下在汇编指令输出的最后,我们发现了异常判断法中发现的内部类,如下图:
上图中箭头很多,一层一层的表达清楚了当前内部类的所有信息。
4、总结
我们总结一下,Lambda 表达式执行主要是依靠 invokedynamic 的 JVM 指令来实现,咱们演示的类的全路径为:demo.eight.Lambda 感兴趣的同学可以自己尝试一下。
以上就是Java源码难点突破Lambda表达式执行原理的详细内容,更多关于Java难点Lambda表达式依靠的资料请关注盛行IT其它相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。