最近刚好在接触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工厂方法,我们去翻到源码

学习顺序
- Lambda表达式;
- JVM字节码;
- java的动态属性;
参考博客:blog.csdn.net/xtayfjpk/ar…
参考书籍: 《深入了解JAVA虚拟机 第二版》