java8 探讨与分析匿名内部类、lambda表达式、方法引用的底层实现

问题解决思路:查看编译生成的字节码文件html

同步发布地址:https://www.cnblogs.com/chenj...java

思路一:express

  1. 编译 javac fileName.java
  2. 反编译 javap -v -p fileName.class ; 这一步能够看到字节码。

思路二:
运行阶段保留jvm生成的类
java -Djdk.internal.lambda.dumpProxyClasses fileName.class数组

不错的博客:https://blog.csdn.net/zxhoo/a...缓存


本人旨在探讨匿名内部类、lambda表达式(lambda expression),方法引用(method references )的底层实现,包括实现的阶段(第一次编译期仍是第二次编译)和实现的原理。markdown

测试匿名内部类的实现

建议去对照着完整的代码来看 源码连接
基于strategy类,使用匿名内部类,main函数的代码以下,称做test1app

Strategy strategy = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy changed : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy);
    s.communicate();

第一步:如今对其使用javac编译,在Strategize.java的目录里,命令行运行javac Strategize.java,结果咱们能够看到生成了5个.class文件,咱们预先定义的只有4个class,而如今却多出了一个,说明编译期帮咱们生成了一个class,其内容以下:jvm

class Strategize$1 implements Strategy {
    Strategize$1() {
    }

    public String approach(String var1) {
        return var1.toUpperCase();
    }
}

第二部:对生成的 Strategize.class 进行反编译,运行javap -v -c Strategize.class,在输出的结尾能够看到下面信息:ide

NestMembers:
  com/langdon/java/onjava8/functional/Strategize$1
InnerClasses:
  #9;                                     // class com/langdon/java/onjava8/functional/Strategize$1

说明,这个Strategize$1的确是Strategize的内部类。
这个类是命名是有规范的,做为Strategize的第一个内部类,因此命名为Strategize$1。若是咱们在测试的时候多写一个匿名内部类,结果会怎样?
咱们修改main()方法,多写一个匿名内部类,称作test2函数

Strategy strategy1 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy1  : "+msg.toUpperCase() + "!";
        }
    };
    Strategy strategy2 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy2  : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy1);
    s.communicate();
    s.changeStrategy(strategy2);
    s.communicate();

继续使用javac编译一下;结果与预想的意义,多生成了2个类,分别是Strategize$1Strategize$2,二者是实现方式是相同的,都是实现了Strategy接口的class

小结

到此,能够说明匿名内部类的实现:第一次编译的时候经过字节码工具多生成一个class来实现的。

测试lambda表达式

第一步:修改test2的代码,把strategy1改用lambda表达式实现,称做test3

Strategy strategy1 = msg -> "strategy1  : "+msg.toUpperCase() + "!";
    Strategy strategy2 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy2  : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy1);
    s.communicate();
    s.changeStrategy(strategy2);
    s.communicate();

第二步:继续使用javac编译,结果只多出了一个class,名为Strategize$1,这是用匿名内部类产生的,可是lambda表达式的实现还看不到。但此时发现main()函数的代码在NetBeans中已经没法反编译出来,是NetBeans的反编译器不够强大?尝试使用在线反编译器,结果的部分以下

public static void main(String[] param0) {
      // $FF: Couldn't be decompiled
   }

   // $FF: synthetic method
   private static String lambda$main$0(String var0) {
      return var0.toUpperCase();
   }

第三步:使用javap反编译,能够看到在main()方法的后面多出了一个函数,以下描述

private static java.lang.String lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #17                 // Method java/lang/String.toUpperCase:()Ljava/lang/String;
         4: invokedynamic #18,  0             // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         9: areturn
      LineNumberTable:
        line 48: 0

到此,咱们只能见到,在第一次编译后仅仅是编译期多生成了一个函数,并无为lambda表达式多生成一个class。
关于这个方法lambda$main$0的命名:以lambda开头,由于是在main()函数里使用了lambda表达式,因此带有$main表示,由于是第一个,因此$0。

第四步:运行Strategize,回到src目录,使用java 完整报名.Strategize,好比我使用的是java com.langdon.java.onjava8.functional.test3.Strategize,结果是直接运行的mian函数,类文件并无发生任何变化。

第五步:加jvm启动属性,若是咱们在启动JVM的时候设置系统属性"jdk.internal.lambda.dumpProxyClasses"的话,那么在启动的时候生成的class会保存下来。使用java命令以下

java -Djdk.internal.lambda.dumpProxyClasses com.langdon.java.onjava8.functional.test3.Strategize

此时,我看到了一个新的类,以下:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$1 implements Strategy {
    private Strategize$$Lambda$1() {
    }

    @Hidden
    public String approach(String var1) {
        return Strategize.lambda$main$0(var1);
    }
}

synthetic class说明这个类是经过字节码工具自动生成的,注意到,这个类是final,实现了Strategy接口,接口是实现很简单,就是调用了第一次编译时候生产的Strategize.lambda$main$0()方法。从命名上能够看出这个类是实现lambda表达式的类和以及Strategize的内部类。

小结

lambda表达式与普通的匿名内部类的实现方式不同,在第一次编译阶段只是多增了一个lambda方法,并经过invoke dynamic 指令指明了在第二次编译(运行)的时候须要执行的额外操做——第二次编译时经过java/lang/invoke/LambdaMetafactory.metafactory 这个工厂方法来生成一个class(其中参数传入的方法就是第一次编译时生成的lambda方法。)
这个操做最终仍是会生成一个实现lambda表达式的内部类。

测试方法引用

为了测试方法引用(method reference),对上面的例子作了一些修改,具体看test4.

第一步:运行javac Strategize.java,并无生产额外的.class文件,都是预约义的。这点与lambda表达式是一致的。但NetBeans对Strategize.class的mian()方法反编译失败,尝试使用上文提到的反编译器,结果也是同样。

第二步:尝试使用javap -v -p 反编译Strategize.class,发现与lambda表达式类似的地方

InnerClasses:
  public static final #82= #81 of #87;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #45 REF_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:
      #46 (Ljava/lang/String;)Ljava/lang/String;
      #47 REF_invokeStatic com/langdon/java/onjava8/functional/test4/Unrelated.twice:(Ljava/lang/String;)Ljava/lang/String;
      #46 (Ljava/lang/String;)Ljava/lang/String;
  1: #45 REF_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:
      #46 (Ljava/lang/String;)Ljava/lang/String;
      #52 REF_invokeVirtual com/langdon/java/onjava8/functional/test4/Unrelated.third:(Ljava/lang/String;)Ljava/lang/String;
      #46 (Ljava/lang/String;)Ljava/lang/String;

从这里能够看出,方法引用的实现方式与lambda表达式是很是类似的,都是在第二次编译(运行)的时候调用java/lang/invoke/LambdaMetafactory.metafactory 这个工厂方法来生成一个class,其中方法引用不须要在第一次编译时生成额外的lambda方法。

第三步:使用jdk.internal.lambda.dumpProxyClasses参数运行。以下

java -Djdk.internal.lambda.dumpProxyClasses com.langdon.java.onjava8.functional.test4.Strategize

结果 jvm 额外生成了2个.class文件,分别是 Strategize\\$\\$Lambda$1Strategize$$Lambda$2, (这里的双斜杠是不存在的,因为markdown语法排版而添加)。从这点能够看出方法引用在第二次编译时的实现方式与lambda表达式是同样的,都是借助字节码工具生成相应的class。两个类的代码以下 (由NetBeans反编译获得)

//for Strategize$$Lambda$1
package com.langdon.java.onjava8.functional.test4;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$1 implements Strategy {
    private Strategize$$Lambda$1() {
    }

    @Hidden
    public String approach(String var1) {
        return Unrelated.twice(var1);
    }
}

// for Strategize$$Lambda$2
package com.langdon.java.onjava8.functional.test4;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$2 implements StrategyDev {
    private final Unrelated arg$1;

    private Strategize$$Lambda$2(Unrelated var1) {
        this.arg$1 = var1;
    }

    private static StrategyDev get$Lambda(Unrelated var0) {
        return new Strategize$$Lambda$2(var0);
    }

    @Hidden
    public String approach(String var1) {
        return this.arg$1.third(var1);
    }
}

小结

方法引用在第一次编译的时候并无生产额外的class,也没有像lambda表达式那样生成一个static方法,而只是使用invoke dynamic标记了(这点与lambda表达式同样),在第二次编译(运行)时会调用java/lang/invoke/LambdaMetafactory.metafactory 这个工厂方法来生成一个class,其中参数传入的方法就是方法引用的实际方法。这个操做与lambda表达式同样都会生成一个匿名内部类。

三种实现方式的总结

方式 javac编译 javap反编译 jvm调参并第二次编译 (运行)
匿名内部类 额外生成class 未见invoke dynamic指令 无变化
lambda表达式 未生成class,但额外生成了一个static的方法 发现invoke dynamic 发现额外的class
方法引用 未额外生成 发现invoke dynamic 发现额外的class

对于lambda表达式,为何java8要这样作?

下面的译本,原文Java-8-Lambdas-A-Peek-Under-the-Hood

匿名内部类具备可能影响应用程序性能的不受欢迎的特性。

  1. 编译器为每一个匿名内部类生成一个新的类文件。生成许多类文件是不可取的,由于每一个类文件在使用以前都须要加载和验证,这会影响应用程序的启动性能。加载多是一个昂贵的操做,包括磁盘I/O和解压缩JAR文件自己。
  2. 若是lambdas被转换为匿名内部类,那么每一个lambda都有一个新的类文件。因为每一个匿名内部类都将被加载,它将占用JVM的元空间(这是Java 8对永久生成的替代)。若是JVM将每一个此类匿名内部类中的代码编译为机器码,那么它将存储在代码缓存中。此外,这些匿名内部类将被实例化为单独的对象。所以,匿名内部类会增长应用程序的内存消耗。为了减小全部这些内存开销,引入一种缓存机制多是有帮助的,这将促使引入某种抽象层。
  3. 最重要的是,从第一天开始就选择使用匿名内部类来实现lambdas,这将限制将来lambda实现更改的范围,以及它们根据将来JVM改进而演进的能力。
  4. 将lambda表达式转换为匿名内部类将限制将来可能的优化(例如缓存),由于它们将绑定到匿名内部类字节码生成机制。

基于以上4点,lambda表达式的实现不能直接在编译阶段就用匿名内部类实现
,而是须要一个稳定的二进制表示,它提供足够的信息,同时容许JVM在将来采用其余可能的实现策略。
解决上述解释的问题,Java语言和JVM工程师决定将翻译策略的选择推迟到运行时。Java 7 中引入的新的 invokedynamic 字节码指令为他们提供了一种高效实现这一目标的机制。将lambda表达式转换为字节码须要两个步骤:

  1. 生成 invokedynamic 调用站点 ( 称为lambda工厂 ),当调用该站点时,返回一个函数接口实例,lambda将被转换到该接口;
  2. 将lambda表达式的主体转换为将经过invokedynamic指令调用的方法。

为了演示第一步,让咱们检查编译一个包含lambda表达式的简单类时生成的字节码,例如:

import java.util.function.Function;

public class Lambda {
    Function<String, Integer> f = s -> Integer.parseInt(s);
}

这将转化为如下字节码:

0: aload_0
 1: invokespecial #1 // Method java/lang/Object."<init>":()V
 4: aload_0
 5: invokedynamic #2, 0 // InvokeDynamic
                  #0:apply:()Ljava/util/function/Function;
10: putfield #3 // Field f:Ljava/util/function/Function;
13: return

注意,方法引用的编译略有不一样,由于javac不须要生成合成方法,能够直接引用方法。

如何执行第二步取决于lambda表达式是非捕获 non-capturing (lambda不访问定义在其主体外部的任何变量) 仍是捕获 capturing (lambda访问定义在其主体外部的变量),好比类成员变量。

非捕获 lambda简单地被描述为一个静态方法,该方法具备与lambda表达式彻底相同的签名,并在使用lambda表达式的同一个类中声明。 例如,上面的Lambda类中声明的lambda表达式能够被描述为这样的方法,这个方法就在使用了lambda表达式的方法的下面生成。

static Integer lambda$1(String s) {
   return Integer.parseInt(s);
}

捕获 lambda表达式的状况要复杂一些,由于捕获的变量必须与lambda的形式参数一块儿传递给实现lambda表达式主体的方法。在这种状况下,常见的转换策略是在lambda表达式的参数以前为每一个捕获的变量添加一个额外的参数。让咱们来看一个实际的例子:

int offset = 100;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset;

能够生成相应的方法实现:

static Integer lambda$1(int offset, String s) {
    return Integer.parseInt(s) + offset;
}

然而,这种翻译策略并非一成不变的,由于使用invokedynamic指令可让编译器在未来灵活地选择不一样的实现策略。例如,能够将捕获的值封装在数组中,或者,若是lambda表达式读取使用它的类的某些字段,则生成的方法能够是实例方法,而不是声明为静态方法,从而避免了将这些字段做为附加参数传递的须要。

理论上的性能

第一步:是连接步骤,它对应于上面提到的lambda工厂步骤。若是咱们将性能与匿名内部类进行比较,那么等效的操做将是装入匿名内部类。Oracle已经发布了Sergey Kuksenko关于这一权衡的性能分析,您能够看到Kuksenko在2013年JVM语言峰会[3]上发表了关于这个主题的演讲。分析代表,预热lambda工厂方法须要时间,在此期间,初始化速度较慢。当连接了足够多的调用站点时,若是代码处于热路径上(即,其中一个频繁调用,足以编译JIT)。另外一方面,若是是冷路径 (cold path),lambda工厂方法能够快100倍。

第二步是:从周围范围捕获变量。正如咱们已经提到的,若是没有要捕获的变量,那么能够自动优化此步骤,以免使用基于lambda工厂的实现分配新对象。在匿名内部类方法中,咱们将实例化一个新对象。为了优化相同的状况,您必须手动优化代码,方法是建立一个对象并将其提高到一个静态字段中。例如:

// Hoisted Function
public static final Function<String, Integer> parseInt = new Function<String, Integer>() {
    public Integer apply(String arg) {
        return Integer.parseInt(arg);
    }
}; 

// Usage:
int result = parseInt.apply(“123”);

第三步:是调用实际的方法。目前,匿名内部类和lambda表达式都执行彻底相同的操做,因此这里的性能没有区别。非捕获lambda表达式的开箱即用性能已经领先于提高的匿名内部类等效性能。捕获lambda表达式的实现与为捕获这些字段而分配匿名内部类的性能相似。

下文将讲述lambda表达式的实如今很大程度上执行得很好。虽然匿名内部类须要手工优化来避免分配,可是JVM已经为咱们优化了这种最多见的状况(一个lambda表达式没有捕获它的参数)。

实测的性能

固然,很容易理解整体性能模型,但在实测中又会是怎样的?咱们已经在一些软件项目中使用了Java 8,并取得了良好的效果。自动优化非捕获lambdas能够提供很好的好处。有一个特定的例子,它提出了一些关于将来优化方向的有趣问题。

所讨论的示例发生在处理系统中使用的一些代码时,这些代码须要特别低的GC暂停(理想状况下是没有暂停)。所以,最好避免分配太多的对象。该项目普遍使用lambdas来实现回调处理程序。不幸的是,咱们仍然有至关多的回调,在这些回调中,咱们没有捕获局部变量,而是但愿引用当前类的一个字段,甚至只是调用当前类的一个方法。目前,这彷佛仍然须要分配。

总结

在本文中,咱们解释了lambdas不只仅是底层的匿名内部类,以及为何匿名内部类不是lambda表达式的合适实现方法。考虑lambda表达式实现方法已经作了大量工做。目前,对于大多数任务,它们都比匿名内部类更快,但目前的状况并不完美;测量驱动的手工优化仍有必定的空间。

不过,Java 8中使用的方法不只限于Java自己。Scala从来经过生成匿名内部类来实现它的lambda表达式。在Scala 2.12中,虽然已经开始使用Java 8中引入的lambda元操做机制。随着时间的推移,JVM上的其余语言也可能采用这种机制。

相关文章
相关标签/搜索