Lambda首次使用很慢怎么办?看法宝,一分钟让你彻底明白MySQL聚簇索引和非聚簇索引

对包含Lambda和不包含的分别执行命令,得到的结果如下:

从日志文件大小来看,就相差了十几kb

注:文件过大,仅展示部分内容

包含Lambda

[Loaded java.lang.invoke.LambdaMetafactory from D:\JDK\jre1.8\lib\rt.jar]

中间省略了很多内容,LambdaMetafactory 是最明显的区别(仅从名字上发现)

[Loaded java.lang.invoke.InnerClassLambdaMetafactory$1 from D:\JDK\jre1.8\lib\rt.jar]

5143 220 4 java.lang.String::equals (81 bytes)

[Loaded java.lang.invoke.LambdaForm$MH/471910020 from java.lang.invoke.LambdaForm]

5143 219 3 jdk.internal.org.objectweb.asm.ByteVector:: (13 bytes)

[Loaded java.lang.invoke.LambdaForm$MH/531885035 from java.lang.invoke.LambdaForm]

5143 222 3 jdk.internal.org.objectweb.asm.ByteVector::putInt (74 bytes)

5143 224 3 com.code.jvm.preheat.Demo$$Lambda$1/834600351::accept (8 bytes)

5143 225 3 com.code.jvm.preheat.Demo::lambda$getTime$0 (6 bytes)

5144 226 4 com.code.jvm.preheat.Demo$$Lambda$1/834600351::accept (8 bytes)

5144 223 1 java.lang.Integer::intValue (5 bytes)

5144 221 3 jdk.internal.org.objectweb.asm.ByteVector::putByteArray (49 bytes)

5144 224 3 com.code.jvm.preheat.Demo$$Lambda$1/834600351::accept (8 bytes) made not entrant

5145 227 % 4 java.util.ArrayList::forEach @ 27 (75 bytes)

5146 3 3 java.lang.String::equals (81 bytes) made not entrant

foreach time one: 50

分割线…

5147 227 % 4 java.util.ArrayList::forEach @ -2 (75 bytes) made not entrant

foreach time two: 1

[Loaded java.lang.Shutdown from D:\JDK\jre1.8\lib\rt.jar]

[Loaded java.lang.Shutdown$Lock from D:\JDK\jre1.8\lib\rt.jar]

不包含Lambda

5095 45 1 java.util.ArrayList::access$100 (5 bytes)

5095 46 1 java.lang.Integer::intValue (5 bytes)

5096 47 3 java.util.ArrayList$Itr::hasNext (20 bytes)

5096 49 3 java.util.ArrayList$Itr::checkForComodification (23 bytes)

5096 48 3 java.util.ArrayList$Itr::next (66 bytes)

5096 50 4 java.util.ArrayList$Itr::hasNext (20 bytes)

5096 51 4 java.util.ArrayList$Itr::checkForComodification (23 bytes)

5096 52 4 java.util.ArrayList$Itr::next (66 bytes)

5097 47 3 java.util.ArrayList$Itr::hasNext (20 bytes) made not entrant

5097 49 3 java.util.ArrayList$Itr::checkForComodification (23 bytes) made not entrant

5097 48 3 java.util.ArrayList$Itr::next (66 bytes) made not entrant

5099 53 % 4 com.code.jvm.preheat.Demo::getTimeFor @ 11 (47 bytes)

5101 50 4 java.util.ArrayList$Itr::hasNext (20 bytes) made not entrant

foreach time one: 7

分割线…

5102 54 3 java.util.ArrayList$Itr::hasNext (20 bytes)

5102 55 4 java.util.ArrayList$Itr::hasNext (20 bytes)

5103 53 % 4 com.code.jvm.preheat.Demo::getTimeFor @ -2 (47 bytes) made not entrant

foreach time two: 1

5103 54 3 java.util.ArrayList$Itr::hasNext (20 bytes) made not entrant

[Loaded java.lang.Shutdown from D:\JDK\jre1.8\lib\rt.jar]

[Loaded java.lang.Shutdown$Lock from D:\JDK\jre1.8\lib\rt.jar]

我们可以结合JIT编译时间,结合JVM载入类的日志发现两个结论:

  1. 凡是使用了Lambda,JVM会额外加载 LambdaMetafactory类,且耗时较长

  2. 在第二次调用Lambda方法时,JVM就不再需要额外加载 LambdaMetafactory类,因此执行较快

完美印证了之前提出的问题:为什么第一次 foreach 慢,以后都很快,但这就是真相吗?我们继续往下看

排除 foreach 的干扰

==================

先来看看 ArrayList中 foreach方法的实现:

@Override

public void forEach(Consumer<? super E> action) {

Objects.requireNonNull(action);

final int expectedModCount = modCount;

@SuppressWarnings(“unchecked”)

final E[] elementData = (E[]) this.elementData;

final int size = this.size;

for (int i=0; modCount == expectedModCount && i < size; i++) {

action.accept(elementData[i]);

}

if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}

}

乍一看,好像也没什么特别,我们来试试把 Consumer 预先定义好,代码如下:

Lambda初次使用很慢?从JIT到类加载再到实现原理

可以发现速度很快,检查 JIT编译时间,检查类加载情况,发现耗时短,且无LambdaMetafactory加载

根据刚才得到的结论,我们试试把 Consumer 用 Lambda的方式定义一下

Consumer consumer = o -> {

int curr = (int) o;

};

执行结果耗时

foreach time: 3

再来看看编译时间和类加载,赫然发现:JIT编译时间较长,且有LambdaMetafactory加载

重新探究Lambda的实现原理

===============

Lambda表达式实现原理的细节,我之后会再出一篇新的文章,今天就先说一下结论:

  • 匿名内部类在编译阶段会多出一个类,而Lambda不会,它仅会多生成一个函数

  • 该函数会在运行阶段,会通过LambdaMetafactory工厂来生成一个class,进行后续的调用

为什么Lamdba要如此实现?

匿名内部类有一定的缺陷:

  1. 编译器为每个匿名内部类生成一个新的类文件,生成许多类文件是不可取的,因为每个类文件在使用之前都需要加载和验证,这会影响应用程序的启动性能,加载可能是一个昂贵的操作,包括磁盘I/O和解压缩JAR文件本身。

  2. 如果lambdas被转换为匿名内部类,那么每个lambda都有一个新的类文件。由于每个匿名内部类都将被加载,它将占用JVM的元空间,如果JVM将每个此类匿名内部类中的代码编译为机器码,那么它将存储在代码缓存中。

  3. 此外,这些匿名内部类将被实例化为单独的对象。因此,匿名内部类会增加应用程序的内存消耗。

  4. 最重要的是,从一开始就选择使用匿名内部类来实现lambdas,这将限制未来lambda实现更改的范围,以及它们根据未来JVM改进而演进的能力。

真相

在理解了匿名内部类以及Lambda表达式的实现原理后,对Lambda耗时长的原因反而更懵逼,毕竟匿名内部类的生成一个新类和Lambda生成一个新方法所耗时间差别不会太多,然后运行期间同样有Class产生,耗时也不应该有太大的区别,到底哪里出现了问题呢?

再次通过查询资料,最终找到了答案:

You are obviously encountering the first-time initialization overhead of lambda expressions. As already mentioned in the comments, the classes for lambda expressions are generated at runtime rather than being loaded from your class path.

However, being generated isn’t the cause for the slowdown. After all, generating a class having a simple structure can be even faster than loading the same bytes from an external source. And the inner class has to be loaded too. But when the application hasn’t used lambda expressions before, even the framework for generating the lambda classes has to be loaded (Oracle’s current implementation uses ASM under the hood). This is the actual cause of the slowdown, loading and initialization of a dozen internally used classes, not the lambda expression itself.

大概翻译过来如下:

显然,您遇到了lambda表达式的首次初始化开销。正如注释中已经提到的,lambda表达式的类是在运行时生成的,而不是从类路径加载的。

然而,生成类并不是速度变慢的原因。毕竟,生成一个结构简单的类比从外部源加载相同的字节还要快。内部类也必须加载。但是,当应用程序以前没有使用lambda表达式时,甚至必须加载用于生成lambda类的框架(Oracle当前的实现在幕后使用ASM)。这是导致十几个内部使用的类(而不是lambda表达式本身)减速、加载和初始化的真正原因。

真相:应用程序初次使用Lambda时,必须加载用于生成Lambda类的框架,因此需要更多的编译,加载的时间

回过头去看看类加载的日志,赫然发现了ASM框架的引入:

[Loaded jdk.internal.org.objectweb.asm.ClassVisitor from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.ClassWriter from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.ByteVector from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.Item from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.MethodVisitor from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.MethodWriter from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.Type from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.Label from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.Frame from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.AnnotationVisitor from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

[Loaded jdk.internal.org.objectweb.asm.AnnotationWriter from F:\Java_JDK\JDK1.8\jre\lib\rt.jar]

结论

==

  • 导致 foreach 测试时数据不正常的罪魁祸首是:Lambda表达式
  • Lambda表达式 在应用程序中首次使用时,需要额外加载ASM框架,因此需要更多的编译,加载的时间
  • Lambda表达式的底层实现并非匿名内部类的语法糖,而是其优化版
  • foreach 的底层实现其实和增强 for循环没有本质区别,一个是外部迭代器,一个是内部迭代器而已
  • 通过 foreach + Lambda 的写法,效率并不低,只不过需要提前进行预热(加载框架)                   希望能帮助到大家!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

总结

其他的内容都可以按照路线图里面整理出来的知识点逐一去熟悉,学习,消化,不建议你去看书学习,最好是多看一些视频,把不懂地方反复看,学习了一节视频内容第二天一定要去复习,并总结成思维导图,形成树状知识网络结构,方便日后复习。

这里还有一份很不错的《Java基础核心总结笔记》,特意跟大家分享出来

目录:

部分内容截图:


)**
[外链图片转存中…(img-0FtprBdA-1711760008698)]

总结

其他的内容都可以按照路线图里面整理出来的知识点逐一去熟悉,学习,消化,不建议你去看书学习,最好是多看一些视频,把不懂地方反复看,学习了一节视频内容第二天一定要去复习,并总结成思维导图,形成树状知识网络结构,方便日后复习。

这里还有一份很不错的《Java基础核心总结笔记》,特意跟大家分享出来

目录:

[外链图片转存中…(img-TCExZK1w-1711760008699)]

部分内容截图:

[外链图片转存中…(img-h7NBjwNi-1711760008699)]

[外链图片转存中…(img-34d5V4gi-1711760008699)]

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值