深入剖析Lambda表达式的底层实现原理

hello,小伙伴们好,我是江湖人送外号[道格牙]的子牙老师。

又有一段时间没有给大家分享文章了,因为最近在筹备创办公司的事情,比较忙。今天偷得浮生半日闲,准备给大家分享下Lambda的底层实现。

如果想研究明白这个问题,我们需要研究哪些东西呢?干想也想不出来对吧,对着代码想吧。

@FunctionalInterface
public interface CustomLambda {
    public void run(int x);
}

public class TestLamda {
    public static void main(String[] args) {
        CustomLambda obj = (x) -> {
            System.out.println("hello#" + x);
        };

        obj.run(1);
    }
}
复制代码

对于这个问题,每个人的答案肯定不一样。但是,好的问题好的切入点是成功的一半。我给自己提了如下这些问题,然后顺着这些问题去研究,研究过程中对于JDK的有些设计或者代码实现不太理解,就尝试自己手写一遍,站在设计者的角度去思考,最终对于Lambda的底层原理有了一定程度的认知。目前我自己的JVM也已支持Lambda表达式。上个我自己的JVM运行Lambda的执行日志给大家瞧瞧

哪些问题呢:

  1. 我们写的Lambda表达式代码,经编译系统编译后是什么样子的?
  2. JVM如何创建出Lambda表达式对应的对象的?
  3. Lambda表达式对应的函数式接口与具体实现中的代码是如何关联上的?
  4. Lambda表达式中的代码段是如何被调用到的?

虽然只有四个问题,但是每个问题展开讲,内容都是蛮多的。刚好也有小伙伴建议我把单篇的文章知识点不要搞得太密集太多,他们消化不良。那就分成两篇文章来讲吧。本篇是第一篇,聚焦分析Lambda表达式的底层实现原理,下篇文章着重讲它的调用细节。

历史背景

研究一个伟大的技术,不了解它的过去不足以更好地理解它的现在甚至它的未来。咱们先来看看Lambda表达式是如何一步步在JVM中生长出来的。

JDK8之前,我们想使用某个接口实现类,要么提前写好实现类,要么使用匿名内部类的方式。提前写好实现类带来的问题一个是项目定义的类会特别多,其次是有些接口中需要实现的方法很少,定义一个实现类显得有点笨重。后来JDK支持了匿名内部类,对于需要实现方法比较少的接口,就直接采用这种方式实现了。一切都显得如何和谐自然。

巴特,重复代码如此严重,免不了被其他语言嘲讽。大佬们就受不了了,免不了再次升级,一场革命酝酿着。于是Lambda表达式诞生了,顺便带来了函数式接口注解@FunctionalInterface。关于这个注解的细节,下篇文章讲。

Lambda表达式的实现依托三个东西:

  1. 匿名内部类(VM Anonymous Class)
  2. invokedynamic
  3. MethodHandle

可以这样说,在Lambda表达式诞生之前,它依托的技术JVM中就已经全部支持了。那为什么Lambda表达式到JDK8才诞生呢?因为需求都是慢慢生长出来的。我们学技术或者做架构也是一样的,你只要不停地去探索你自己的未知领域,你才能获得成长需求,你才知道要学什么,你才能真正的成长。

这里还分主动成长与被动成长。主动成长是说有的人他当前的能力已经满足工作需要,但是他居安思危,去看更高薪资的岗位职能要求或者从技术的长远角度出发去思考自己当前的欠缺,针对性补充。这样的人不会有35岁瓶颈,才有更广阔的未来,但是这样的人,其实很少。大多数人都是被动成长,需要别人告诉他什么什么很重要,你当前阶段需要储备什么,他还将信将疑。真正到了某个危机时刻,他会感叹一句:当初听你的就好了。大家回想一下,自己或者身边人,是不是好多是这样的。

希望看到我这篇文章的小伙伴都是,或者改变思维,做第一种人。当你真正改变了思维,成为了这样的人,你会发现,别人看你的眼光、你慢慢能够获得的,真的会不一样。做任何事情,成功的都是回归了人之初心的人。任何的理性、功利心…最终都会走向失败。这是我这么多年看过的诸多人结合自己的成长获得的心得体会。

字节码文件层面

接下来回答第一个问题:我们写的Lambda表达式代码,经编译系统编译后是什么样子的?

1、常量池中会产品一项:JVM_CONSTANT_InvokeDynamic。

2、类属性会多出一项:BootstrapMethods。

3、常量池中还会出现两个JVM_CONSTANT_MethodHandle,一个JVM_CONSTANT_MethodType。

4、会多出一个方法。这个方法是编译器自动生成的。所以可以这样说,Lambda表达式的实现,是编译系统与运行系统互相配合实现的。

5、Lambda表达式的调用指令是invokedynamic

这些就是你的Java代码中有Lambda表达式会多出来的东西。可以想象,Lambda表达式实现起来还是比较复杂的。那JVM在执行Lambda表达式的代码时,是如何将这些元素结合起来的呢?接着往后看。

如何实现调用

在网上看相关文章的时候,看到一张图,直接拿过来用了。这张图基本画出了JVM执行Lambda表达式的执行逻辑。接下来我详细解释下。

JVM执行Lambda表达式的执行逻辑,最复杂的部分是JVM执行字节码指令invokedynamic的逻辑。后面的逻辑其实就是多态调用接口方法。接下来我就把最复杂的部分细讲下。

1、通过indy后面的操作数,拿到常量池中的信息:JVM_CONSTANT_InvokeDynamic。大家有没有注意到,我们Java代码中的run方法返回类型是void,编译之后却是CustomLambda。言外之意,这一步执行完会创建CustomLambda的实现类对象。

2、从常量池相JVM_CONSTANT_InvokeDynamic中拿到BootstrapMethods的索引。即我们目前调用的是第几个BootstrapMethod。

3、BootstrapMethod结构中的Bootstrap方法,对应的常量池项是MethodHandle。JVM就是通过执行LambdaMetafactory.metafactory创建出CallSite,进而创建出Lambda表达式对应的对象的。BootstrapMethod结构中的其他信息,都是一些辅助信息,是调用metafactory方法需要传的参数。

4、JVM也是通过执行LambdaMetafactory.metafactory完成Lambda表达式对应的函数式接口与具体实现中的代码的关联。背后的实现原理,对,就是玩字节码。加上参数-Djdk.internal.lambda.dumpProxyClasses可以将生成的类保存到文件中。看下生成的文件内容长啥样子。

总结来说就是JVM通过执行LambdaMetafactory.metafactory完成Lambda表达式对应的函数式接口与具体实现中的代码的关联,默认的,在内存中,会生成一个新的类,并返回这个类的实例,所以可以这样调用run方法

obj.run(1);
复制代码

纸上得来终觉浅,绝知此事要躬行。大家有空可以自己写写看,还是非常有意思的。

结语

我是子牙老师,喜欢钻研底层,深入研究Windows、Linux内核、JVM。运营公众号:硬核子牙

Guess you like

Origin juejin.im/post/7031368690624888869