写在前面

本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定Java虚拟机

解答

在这里插入图片描述

在 Java 8 中,Lambda 表达式是借助 invokedynamic 指令来实现的。
invokedymaic 指令抽象出调用点的概念,并且将调用该调用点所链接的方法句柄。
在第一次执行 invokedynamic 指令时,Java 虚拟机将执行它所对应的启动方法,生成并且绑定一个调用点。
之后如果再次执行该指令,Java 虚拟机则直接调用已经绑定了的调用点所链接的方法。

补充

函数式接口

函数式接口指的是仅包括一个非 default 接口方法的接口,一般通过 @FunctionalInterface 注解。

就算是没有使用该注解,Java 编译器也会将符合条件的接口辨认为函数式接口。

实现原理

第一次执行 invokedynamic 指令时,它所对应的启动方法会通过 ASM 来生成一个适配器类。

这个适配器类实现了对应的函数式接口。启动方法的返回值是一个 ConstantCallSite,其链接对象为一个返回适配器类实例的方法句柄。

根据 Lambda 表达式是否捕获其他变量,启动方法生成的适配器类以及所链接的方法句柄皆不同。

如果该 Lambda 表达式没有捕获其他变量,那么可以认为它是上下文无关的。

因此,启动方法将新建一个适配器类的实例,并且生成一个特殊的方法句柄,始终返回该实例。

如果该 Lambda 表达式捕获了其他变量,那么每次执行该 invokedynamic 指令,我们都要更新这些捕获了的变量,以防止它们发生了变化。

另外,为了保证 Lambda 表达式的线程安全,我们无法共享同一个适配器类的实例。

因此,在每次执行 invokedynamic 指令时,所调用的方法句柄都需要新建一个适配器类实例。

在这种情况下,启动方法生成的适配器类将包含一个额外的静态方法,来构造适配器类的实例。

该方法将接收这些捕获的参数,并且将它们保存为适配器类实例的实例字段。

Lambda 表达式对性能有影响吗?

Lambda 与直接调用的性能并无太大的区别。

Lambda 表达式所使用的 invokedynamic 将绑定一个 ConstantCallSite,其链接的目标方法无法改变。

因此,即时编译器会将该目标方法直接内联进来。

对于没有捕获其他变量的 Lambda 表达式,该 invokedynamic 指令始终返回同一个适配器类的实例。

对于捕获了其他变量的 Lambda 表达式,每次执行 invokedynamic 指令将新建一个适配器类实例。

不管是捕获型的还是未捕获型的 Lambda 表达式,它们的性能上限皆可以达到直接调用的性能。

其中,捕获型 Lambda 表达式借助了即时编译器中的逃逸分析,来避免实际的新建适配器类实例的操作。

我们应当尽量使用非捕获的 Lambda 表达式

上一篇 下一篇