从字节码看Lambda表达式本质

2,347 阅读5分钟

最近刚好在接触JAVA语言的动态特性以及JVM虚拟机字节码,因此选择Lambda表达式作为切入点来探究一下,Lambda表达式的JVM底层特性,本篇文章仅作为个人记录的学习笔记。

大家都知道,JAVA在JDK1.7的时候引入了java.lang.invoke包,使得JVM虚拟机同时具备了静态语言的严谨性以及动态语言的灵活性,这个特性,其实也是JDK1.8中Lambda的基础。我们先来简单看下java.lang.invoke的一些简单用法以及JVM虚拟机方法执行指令,作为本篇文章的理论基础。

灵活的方法句柄-MethodHandle

在jdk1.7之前,所有方法都是绑定在对象之上,但java.lang.invoke中,面向对象的思想得到了进一步的扩展,方法成了一个更加独立的对象,一个方法可以它的参数值和它的返回值确定下来,而与方法所在的对象无关。如下引入一段简单代码。

    @Test
    public void testMethodHadle() throws Throwable {
        //模拟虚拟机方法的查找和调用
        MethodHandles.Lookup looup = MethodHandles.lookup();
        //方法类型,唯一确认这个方法的返回值和参数值,下面这段代码代表这个方法返回值为int,接受一个String类型的参数
        MethodType methodType = MethodType.methodType(int.class,String.class);
        //查找String这个class下,方法名称为indexOf,并且方法类型与前面一致的方法,获得引用
        MethodHandle methodHandle = looup.findVirtual(String.class,"indexOf",methodType);
        //调用,其实就是"hello netease".indexOf("netease")
        int index = (int) methodHandle.invoke("hello netease","netease");
        Assert.assertEquals(6,index);
    }

由于只关心方法类型,而与具体的对象脱钩,因此在此基础上可以发展出更加灵活通用的方法,例如任何一个对象只要有方法返回值为int,接受一个String类型的参数,那就可以公用同一个方法类型,其实这种格式通过反射也能同样处理,读者有兴趣可以自行去探索下区别。在JDK.8中List接口新增的Stream方法,就是通过Lambda表达式内部使用了该方法接受一个函数参数,从而对数据进行流畅处理;

List<String> above90Names = students.stream()
        .filter(t->t.getScore()>90) //过滤,筛选分数大于90
        .peek(System.out::println)  //调试打印出每个学生信息
        .map(Student::getName)     //获取学生的姓名
        .collect(Collectors.toList()); //最终转化为List

虚拟机执行方法的本质

所有方法在字节码层次的执行可以归纳为如下几个指令,读者可以对class文件执行javap -v指令反编译查看,推荐阅读《深入了解JAVA虚拟机一书》。

invokestatic:调用类的静态方法

invokespecial:调用实例构造器方法、父类方法和私有方法

invokevirtua: 调用所有的虚方法

invokeinterface: 调用接口方法,会在运行时再具体确定接口的实现类

invokedynamic: 动态方法调用,具体的实现方法由虚拟机的引导类确认

前四种方法的执行都是由虚拟机制定方法的实际实现对象,而最后一个指令invokedynamic即是动态特性的基础。接下来我们以Lambda表达式为例分析这一个invokedynamic;

反编译后的lambda表达式

我们以一个基础的基础的表达式;

    @Test
    public void testLambda()
    {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //Lambda表达式,实现Runnable接口
        executorService.submit(() -> System.out.println("hello World!")); 
        executorService.shutdown();
    }

对这段java代码生成的class文件进行反编译(javap -v 命令)可得相关方法的字节码指令,我们着重看方法的CODE属性,为便于理解,每行字节码都加了中文注释:

    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
        //调用newSingleThreadExecutor这个静态方法
         0: invokestatic  #2                  // Method java/util/concurrent/Exe                                                                                                                cutors.newSingleThreadExecutor:()Ljava/util/concurrent/ExecutorService;
         //将方法返回的对象保存至第二个局部变量
         3: astore_1
         //从局部变量中读出第二个变量入栈
         4: aload_1
         //lambda表达式核心逻辑
         5: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;             
         //调用executorService的接口实现类                                   
        10: invokeinterface #4,  2            // InterfaceMethod java/util/concu                                                                                                                rrent/ExecutorService.submit:(Ljava/lang/Runnable;)Ljava/util/concurrent/Future;
        15: pop
        16: aload_1
        17: invokeinterface #5,  1            // InterfaceMethod java/util/concu                                                                                                                rrent/ExecutorService.shutdown:()V
        22: return
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 16
        line 13: 22
        //方法的局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            4      19     1 executorService   Ljava/util/concurrent/ExecutorServ 
    BootstrapMethods:
  0: #31 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang                                                                                                                /invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljav                                                                                                                a/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/Method                                                                                                                Type;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #32 ()V
      #33 invokestatic com/lrdong/demo/simple/queue/LambdaTest.lambda$main$0:()V
      #32 ()V

                                                                                                               

对比源代码,我们发现lambd表达式的那一句在字节码层面被替换成了invokedynamic #3, 0 ,#3为字节码指令的助记符,指向常量池,我们继续查看该class文件的常量池属性

Constant pool:
   #1 = Methodref          #10.#27        // java/lang/Object."<init>":()V
   #2 = Methodref          #28.#29        // java/util/concurrent/Executors.newS                                                                                                                ingleThreadExecutor:()Ljava/util/concurrent/ExecutorService;
   #3 = InvokeDynamic      #0:#34         // #0:run:()Ljava/lang/Runnable;
   #4 = InterfaceMethodref #35.#36        // java/util/concurrent/ExecutorServic                                                                                                                
   ...
     #34 = NameAndType        #50:#51        // run:()Ljava/lang/Runnable;

可见#3在常量池中代表的方法是 InvokeDynamic #0:#34 ,根据虚拟机定义的InvokeDynamic常量的存储属性可知可以得到

#0:对应BootstrapMethods引导方法的第一个索引,即

BootstrapMethods:
  0: #31 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang                                                                                                                /invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljav                                                                                                                a/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/Method                                                                                                                Type;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #32 ()V
      #33 invokestatic com/lrdong/demo/simple/queue/LambdaTest.lambda$main$0:()V
      #32 ()V

#34 根据常量池索引可知代表run:()Ljava/lang/Runnable;runnable接口的符号索引;

分析引导方法可知,虚拟机调用了LambdaMetafactory类的metafactory工厂方法,我们去翻到源码

核心就是创建了一个InnerClassLambdaMetafactory,返回值为CallSite,进一步查看InnerClassLambdaMetafactory类的方法可知,InnerClassLambdaMetafactory实现了Runnable方法,而CallSite的作用即充当一个连接点,每个出现的invokedynamic指令都成为一个动态调用点(dynamic call site)。每个动态调用点在初始化的时候,都处于未链接的状态。在这个时候,这个动态调用点并没有被指定要调用的实际方法。当虚拟机要执行dynamic指令时,首先要链接到动态调用点,CallSite上会绑定一个MethodHandle,通过MethodHandle方法句柄就可以定位到真正在执行的方法。也就是在最终阶段又回到了灵活的方法句柄-MethodHandle-一章中介绍的基础方法;

学习顺序

  • Lambda表达式;
  • JVM字节码;
  • java的动态属性;

参考博客:blog.csdn.net/xtayfjpk/ar…

参考书籍: 《深入了解JAVA虚拟机 第二版》