写点什么

☕️【Java 技术之旅】带你看透 Lambda 表达式的底层

发布于: 2021 年 05 月 27 日
☕️【Java 技术之旅】带你看透Lambda表达式的底层

每日一句

只要下定决心,过去的失败,正好是未来行动的借鉴;只要不屈不挠,一时的障碍,正好是推动成功的力量。

前提回顾

之前写完了一篇【Java 技术之旅】带你看透 Lambda 表达式的本质】 https://xie.infoq.cn/article/f859764824f443776bc95fd1e

相比较本篇而言是姊妹篇,本篇是基于上一篇的与案例分析,更加深度的分析了一下原理,本篇是主要针对于 Lambda 表达式的实现原理进行更低层分析,让读者可以更加底层面认识一下 Lambda 的原理。

笔者建议

希望先看完【https://xie.infoq.cn/article/f859764824f443776bc95fd1e】再看本篇,会更加符合循序渐进。

Lambda 的原理

Java 8 支持动态语言,看到很酷的 Lambda 表达式,对一直以静态类型语言自居的 Java,让人看到了 Java 虚拟机可以支持动态语言的目标。

Lambda 的案例

import java.util.function.Consumer;public class Lambda {  public static void main(String[] args) {    Consumer<String> c = s -> System.out.println(s);    c.accept("hello lambda!");  }}
复制代码

Lambda 表达式

刚看到这个表达式,感觉 java 的处理方式是属于内部匿名类的方式


public class Lambda {  static {    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");  }  public static void main(String[] args) {    Consumer<String> c = new Consumer<String>(){      @Override      public void accept(String s) {        System.out.println(s);      }      };    c.accept("hello lambda");  }}
复制代码


编译的结果应该是 Lambda.class , Lambda$1.class 猜测在支持动态语言 java 换汤不换药,在最后编译的时候生成我们常见的方式。但是结果不是这样的,只是产生了一个 Lambda.class


反编译吧,来看看真相是什么?


javap -v -p Lambda.class 
复制代码


  • -p 参数会显示所有的方法,而不带默认是不会反编译 private 的方法的。


public Lambda();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #21                 // Method java/lang/Object."<init>":()V         4: return      LineNumberTable:        line 3: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   LLambda;  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=2, args_size=1         0: invokedynamic #30,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;         5: astore_1         6: aload_1         7: ldc           #31                 // String hello lambda         9: invokeinterface #33,  2           // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V        14: return      LineNumberTable:        line 8: 0        line 9: 6        line 10: 14      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      15     0  args   [Ljava/lang/String;            6       9     1     c   Ljava/util/function/Consumer;      LocalVariableTypeTable:        Start  Length  Slot  Name   Signature            6       9     1     c   Ljava/util/function/Consumer<Ljava/lang/String;>;   private static void lambda$0(java.lang.String);    descriptor: (Ljava/lang/String;)V    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC    Code:      stack=2, locals=1, args_size=1         0: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;         3: aload_0         4: invokevirtual #50                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V         7: return      LineNumberTable:        line 8: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       8     0     s   Ljava/lang/String; }SourceFile: "Lambda.java"BootstrapMethods:  0: #66 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;    Method arguments:      #67 (Ljava/lang/Object;)V      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V      #71 (Ljava/lang/String;)VInnerClasses:     public static final #77= #73 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
复制代码

重点关注方法

Invokedynamic
  • Java 的调用函数的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符号引用在静态类型语言编译时就能产生。

  • 动态类型语言只有在运行期才能确定接收者类型,改变四大指令的语意对 java 的版本有很大的影响,所以在 JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一个新的指令:Invokedynamic。


// InvokeDynamic #0:accept:()Ljava/util/function/Consumer;0: invokedynamic #30,0
复制代码


  • #30 是代表常量 #30 也就是后面的注释 InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

  • 0 是占位符号,目前无用

BootstrapMethods

每一个 invokedynamic 指令的实例叫做一个动态调用点(dynamic call site),动态调用点最开始是未链接状态(unlinked):表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。


引导方法是由编译器生成,在运行期当 JVM 第一次遇到 invokedynamic 指令时, 会调用引导方法来将 invokedynamic 指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为

CallSite

引导方法的返回值类型是 java.lang.invoke.CallSite,一个 invokedynamic 指令关联一个 CallSite,将所有的调用委托到 CallSite 当前的 target(MethodHandle)


InvokeDynamic #0 就是 BootstrapMethods 表示 #0 的位置


  0: #66 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;    Method arguments:      #67 (Ljava/lang/Object;)V      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V      #71 (Ljava/lang/String;)V
复制代码


我们看到调用了 LambdaMetaFactory.metafactory 的方法


参数:


  • LambdaMetafactory.metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType) 有六个参数, 按顺序描述如下

  • MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用 invokedynamic 指令时, JVM 会自动自动填充这个参数。

  • String invokedName : 要实现的方法的名字, 使用 invokedynamic 时, JVM 自动帮我们填充(填充内容来自常量池 InvokeDynamic.NameAndType.Name), 在这里 JVM 为我们填充为 "apply", 即 Consumer.accept 方法名

  • MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法 signature)

  • 使用 invokedynamic 指令时, JVM 会自动自动填充这个参数(填充内容来自常量池 InvokeDynamic.NameAndType.Type), 在这里参数为 String, 返回值类型为 Consumer, 表示这个调用点的目标方法的参数为 String, 然后 invokedynamic 执行完后会返回一个即 Consumer 实例

  • MethodType samMethodType :  函数对象将要实现的接口方法类型,这里运行时, 值为 (Object)Object 即 Consumer.accept 方法的类型(泛型信息被擦除)。#67 (Ljava/lang/Object;)V

  • MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数)

  • 在这里为 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.

  • MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型:

  • 比如函数接口方法定义为 void accept(T t) T 为泛型标识, 这个时候方法类型为(Object)Void。

  • 在编译时 T 已确定, 即 T 由 String 替换, 这时 samMethodType 就是 (Object)Void,

  • instantiatedMethodType 为(String)Void


第 4,5,6 三个参数来自 class 文件中的。如上面引导方法字节码中 Method arguments 后面的三个参数就是将应用于 4, 5, 6 的参数。


  Method arguments:      #67 (Ljava/lang/Object;)V      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V      #71 (Ljava/lang/String;)V
复制代码


public static CallSite metafactory(MethodHandles.Lookup caller,                                       String invokedName,                                       MethodType invokedType,                                       MethodType samMethodType,                                       MethodHandle implMethod,                                       MethodType instantiatedMethodType)            throws LambdaConversionException {        AbstractValidatingLambdaMetafactory mf;        mf = new InnerClassLambdaMetafactory(caller, invokedType,                                             invokedName, samMethodType,                                             implMethod, instantiatedMethodType,                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);        mf.validateMetafactoryArgs();        return mf.buildCallSite();}
复制代码


在 buildCallSite 的函数中


CallSite buildCallSite() throws LambdaConversionException {        final Class<?> innerClass = spinInnerClass();
复制代码


函数 spinInnerClass 构建了这个内部类,也就是生成了一个 Lambda$1/716157500 这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数


System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
复制代码


会在你指定的路径 ,当前运行路径上生成这个内部类

静态类

Java 在编译表达式的时候会生成 lambda$0 静态私有类方法,在这个方法里实现了表达式中的方法块 system.out.println(s);


private static void lambda$0(java.lang.String);    descriptor: (Ljava/lang/String;)V    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC    Code:      stack=2, locals=1, args_size=1         0: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;         3: aload_0         4: invokevirtual #50                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V         7: return      LineNumberTable:        line 8: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       8     0     s   Ljava/lang/String;
复制代码


当然了在上一步通过设置的 jdk.internal.lambda.dumpProxyClasses 里生成的


Lambda$$Lambda$1.class public void accept(java.lang.Object);    descriptor: (Ljava/lang/Object;)V    flags: ACC_PUBLIC    Code:      stack=1, locals=2, args_size=2         0: aload_1         1: checkcast     #15                 // class java/lang/String         4: invokestatic  #21                 // Method Lambda.lambda$0:(Ljava/lang/String;)V         7: return    RuntimeVisibleAnnotations:      0: #13()
复制代码


调用了 Lambda.lambda$0 静态函数,也就是表达式中的函数块

总结

这样就完成的实现了 Lambda 表达式,


  1. 使用 invokedynamic 指令,运行时调用 LambdaMetafactory.metafactory 动态的生成内部类,实现了接口

  2. 内部类里的调用方法块并不是动态生成的,只是在原 class 里已经编译生成了一个静态的方法,内部类只需要调用该静态方法

发布于: 2021 年 05 月 27 日阅读数: 533
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕️【Java 技术之旅】带你看透Lambda表达式的底层