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

思路一:

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

思路二:

运行阶段保留jvm生成的类

java -Djdk.internal.lambda.dumpProxyClasses fileName.class

不错的博客:https://blog.csdn.net/zxhoo/article/category/1800245


本人旨在探讨匿名内部类、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$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$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

对于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上的其他语言也可能采用这种机制。

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

  1. 函数式接口 & lambda表达式 & 方法引用

    拉呱: 终于,学习jdk8的新特性了,初体验带给我的感觉真爽,代码精简的不行,可读性也很好,而且,spring5也是把jdk8的融入到血液里,总之一句话吧,说的打趣一点,学的时候自己难受,学完了写出来 ...

  2. java8之lambda表达式&方法引用(一)

    本文将简单的介绍一下Lambda表达式和方法引用,这也是Java8的重要更新,Lambda表达式和方法引用最主要的功能是为流(专门负责迭代数据的集合)服务. 什么是lambda表达式 可以把lambd ...

  3. 黑马Lambda表达式学习 Stream流 函数式接口 Lambda表达式 方法引用

  4. 黑马方法引用学习 Stream流 函数式接口 Lambda表达式 方法引用

  5. 黑马函数式接口学习 Stream流 函数式接口 Lambda表达式 方法引用

  6. 黑马Stream流学习 Stream流 函数式接口 Lambda表达式 方法引用

  7. lambda与方法引用

    哈喽,大家好,我是指北君. 虽然目前Java最新版本都已经到16了,但是绝大部分公司目前用的Java版本都是8,想当初Java8问世后,其Lambda表达式与方法引用可是最亮眼的新特性,目前,这两个特 ...

  8. Java8特性之Lambda、方法引用以及Stream流

    Java 8 中的 Streams API 详解:https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/ Java笔记——Jav ...

  9. Java8新特性探索之Lambda表达式

    为什么引入Lambda表达式? Lambda 表达式产生函数,而不是类. 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 lamb ...

随机推荐

  1. python之robotframework+pycharm测试框架

    一.robotframework简介 Robot Framework是一款python编写的功能自动化测试框架.具备良好的可扩展性,支持关键字驱动,可以同时测试多种类型的客户端或者接口,可以进行分布式 ...

  2. springboot 启动报错"No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available"

    1.问题 springboot启动报错 "D:\Program Files\Java\jdk-11\bin\java.exe" -XX:TieredStopAtLevel=1 -n ...

  3. 插入与读取Blob类型数据

    BlobTest package com.aff.PreparedStatement; import java.io.File; import java.io.FileInputStream; imp ...

  4. Java实现 LeetCode 654 最大二叉树(递归)

    654. 最大二叉树 给定一个不含重复元素的整数数组.一个以此数组构建的最大二叉树定义如下: 二叉树的根是数组中的最大元素. 左子树是通过数组中最大值左边部分构造出的最大二叉树. 右子树是通过数组中最 ...

  5. Java实现 蓝桥杯VIP 算法训练 P1101

    有一份提货单,其数据项目有:商品名(MC).单价(DJ).数量(SL).定义一个结构体prut,其成员是上面的三项数据.在主函数中定义一个prut类型的结构体数组,输入每个元素的值,计算并输出提货单的 ...

  6. java实现测量到的工程数据

    [12,127,85,66,27,34,15,344,156,344,29,47,-] 这是某设备测量到的工程数据. 因工程要求,需要找出最大的 5 个值. 一般的想法是对它排序,输出前 5 个.但当 ...

  7. Java实现 蓝桥杯 历届试题 危险系数

    问题描述 抗日战争时期,冀中平原的地道战曾发挥重要作用. 地道的多个站点间有通道连接,形成了庞大的网络.但也有隐患,当敌人发现了某个站点后,其它站点间可能因此会失去联系. 我们来定义一个危险系数DF( ...

  8. PAT 挖掘机技术哪家强

    为了用事实说明挖掘机技术到底哪家强,PAT 组织了一场挖掘机技能大赛.现请你根据比赛结果统计出技术最强的那个学校. 输入格式: 输入在第 1 行给出不超过 105 的正整数 N,即参赛人数.随后 N  ...

  9. 逐行解读HashMap源码

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 一.写在前面 相 ...

  10. pi-star 升级固件命令

    单天线热点: sudo pistar-mmdvmhshatflash hs_hat 双天线热点: sudo pistar-mmdvmhshatflash hs_dual_hat 命令: wget ht ...