[TOC]
javac fileName.java javap -v -p fileName.class
思路二:
运行阶段保留jvm生成的类
java -Djdk.internal.lambda.dumpProxyClasses fileName.class
不错的博客: https://blog.csdn.net/zxhoo/a...
本人旨在探讨匿名内部类、lambda表达式(lambda expression),方法引用(method references )的底层实现,包括实现的阶段(第一次编译期还是第二次编译)和实现的原理。
建议去对照着完整的代码来看 源码链接
基于strategy类,使用匿名内部类,main函数的代码如下,称作test1
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,其内容如下:
class Strategize$1 implements Strategy { Strategize$1() { } public String approach(String var1) { return var1.toUpperCase(); } }
第二部:对生成的 Strategize.class
进行反编译,运行 javap -v -c Strategize.class
,在输出的结尾可以看到下面信息:
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$1
和 Strategize$2
,两者是实现方式是相同的,都是实现了 Strategy
接口的 class
。
到此,可以说明匿名内部类的实现:第一次编译的时候通过字节码工具多生成一个class来实现的。
第一步:修改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$1 与 Strategize$$Lambda$2。从这点可以看出方法引用在第二次编译时的实现方式与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 |
下面的译本,原文 Java-8-Lambdas-A-Peek-Under-the-Hood
匿名内部类具有可能影响应用程序性能的不受欢迎的特性。
基于以上4点,lambda表达式的实现不能直接在编译阶段就用匿名内部类实现
,而是需要一个稳定的二进制表示,它提供足够的信息,同时允许JVM在未来采用其他可能的实现策略。
解决上述解释的问题,Java语言和JVM工程师决定将翻译策略的选择推迟到运行时。Java 7 中引入的新的 invokedynamic
字节码指令为他们提供了一种高效实现这一目标的机制。将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上的其他语言也可能采用这种机制。