描述的话不多说,直接上图:
看到输出结果了吗?为什么第一次和第二次的时间相差如此之多?咱们一起琢磨琢磨,也可以先去看看结论再回过头看分析
注:并非仅第二次快,而是除了第一次,之后的每一次都很快
给与猜想
- 是否和操作系统预热有关?
- 是否和JIT(即时编译)有关?
- 是否和ClassLoader类加载有关?
- 是否和Lambda有关,并非foreach的问题
验证猜想
操作系统预热
操作系统预热这个概念是我咨询一位大佬得到的结论,在百度 / Google 中并未搜索到相应的词汇,但是在模拟测试中,我用 普通遍历 的方式进行测试:
基本上每次都是前几次速度较慢,后面的速度更快,因此 可能 有这个因素影响,但差距并不会很大,因此该结论并不能作为问题的答案。
JIT 即时编译
首先介绍一下什么是JIT即时编译:
当 JVM 的初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。
最初,JVM 中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为热点代码。
为了提高热点代码的执行效率,在运行时,即时编译器(JIT,Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中
再来一个概念,回边计数器
回边计数器用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为 "回边"(Back Edge)
建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译,在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言
从上述的概念中,我们应该可以得到一个结论:第一条所谓的操作系统预热 大概率不正确,因为普通遍历方法执行N次,后续执行的时间占用比较小,很可能是因为JIT导致的。
那 JIT即时编译 是否是最终的答案?我们想办法把 JIT 关掉来测试一下,通过查询资料发现了如下内容:
Procedure
- Use the -D option on the JVM command line to set the java.compiler property to NONE or the empty string. Type the following command at a shell or command prompt: java -Djava.compiler=NONE <class> 复制代码
注:该段内容来自IBM官方资料,地址见 <收获> ,咱们先不要停止思考
通过配置 IDEA JVM 参数:
执行问题中的代码测试结果如下:
# 禁用前
foreach time one: 38
分割线...
foreach time two: 1
# 禁用后
forea