416天前,我以为这是编程玄学... (下)

简介: 416天前,我以为这是编程玄学... (下)

non-final case 的截图是这样的:

image.png

顺道说一句题外话,截图来源就是 JITWatch 工具,一个很强大的工具。

从你的截图来看,虽然 runMethod 都被编译过了,但是并没有被真正的执行过。你需要注意的是汇编输出中有 % 标记的地方,它代表着 OSR(on-stack replacement)栈上替换。

如果你不清楚啥是 OSR 也先别着急,一会说。

对于加和不加 final,最终得出的汇编代码是不一样的,我编译之后,仅保留相关部分如下:

image.png

从截图中可以看出,没有加 final 的时候,汇编代码其实就是一个死循环。而加上 final 之后,每次都会去加载 flag 字段。

但是你看,这两种情况,都没有对 Simple 类进行实例分配,也没有字段的分配。

所以,这不是编译器 final 字段赋值的问题,而是编译器的一种优化手段。

整个过程中完全没有 Simple 类的事儿,也就更加没有 final 字段的事儿了。但是加上 final 之后确实影响了程序的结果。

这个问题在比较新的 JVM 版本中得到了修复(言外之意就是一个 BUG?)。

所以,如果你在 JDK 11 版本上运行相同的代码,无论加不加 final,程序都不会正常退出。

好了,上面说了这么多,其实原因已经很清楚了。

根本原因是因为加不加 final 在我的示例环境中生成的是两套不同的机器码。

深层次的原因是 OSR 机制导致的。



验证


经过前面的分析,现在新的排查方向又出来了。

我现在得去验证一下回答问题这个哥们是不是在胡说。

于是我先去验证了他的这句话:

If you run the same example on JDK 11, there will be an infinite loop in both cases, regardless of the final modifier.

用高版本的 JDK 分别运行加了 final 和不加 final 修饰符的情况。

程序确实是都陷入了死循环。

动图如下,可以看到我的 JDK 版本是 15.0.1:

image.png

第一个点验证完成。同样的代码,JDK8 和 JDK15 运行起来结果不一致(其实JDK9运行就不一致了)。

我有理由相信,也许这是 JVM 的一个,不能说 BUG,应该说是缺陷吧。(等等...缺陷不就是 BUG 吗?)

第二个验证的点是他的这句话:

Instead, execution jumps from the interpreter to the OSR stub.

用 JDK8 跑出来结果不一样是因为有栈上替换在捣鬼,那么我可以用下面这个命令,把栈上替换给关闭了:

-XX:-UseOnStackReplacement

去掉 final 后,再次运行程序,程序停止了。

第二个点验证完成。

第三个验证的点是他的这个地方:


image.png

我也把我的汇编搞出来看看,有没有类似这样的地方。

怎么搞汇编出来呢?

用下面这个命令:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log

同时你还需要一个 hsdis 的 dll 文件,网上有很多,一搜就能找到,我相信如果你也想亲自验证,那么找这个文件难不倒你。

没有加 final 字段的时候,汇编是这样的:

image.png

jmp 指令是干啥的?

无条件跳转。

所以,这里就是个死循环。

加上 final 字段后,汇编是这样的:

image.png

首先跳转用的是 je 了,而不是 jmp 了。

je 的跳转是有条件的,代表的是“等于则跳转”。

而在 je 指令之前,还有 movzbl 指令,该操作就是在读取 flag 变量的值。

所以,加了 final 语句之后,每次都会去读取 flag 变量的值,因此 flag 值的变化能及时被主线程看到。

同时我也有 JITWatch 看了一下,对于循环中的 new Why(18) 语句,编译器分析出来这句话并没有什么卵用,于是被优化掉了:


image.png

所以我们在汇编中没有看到对 Why 对象进行分配的相关指令,也就是验证了他的这句话:

You see, in both cases there is no Simple instance allocation at all, and no field assignment either.

自此,玄学问题得到了科学的解释。

如果你坚持看到了这里,那么恭喜你,又学到了一个没啥卵用的知识点。

如果你想要学点和本文相关的、有用的东西,那么我建议看看这几个地方:

  • 《Java并发编程的艺术》的3.6小节-final域的内存语义。
  • 《深入理解Java虚拟机》的第四部分-程序编译与代码优化。
  • 《深入解析Java虚拟机HotSpot》的第7章-编译概述,第8章-C1编译器,第9章-C2编译器。
  • 《Java性能优化实践》的第10章-理解即时编译

看完上面这些之后,你至少会比较清楚的了解到 Java 程序从源码编译成字节码,再从字节码编译成本地机器码的这两个过程。

能够了解 JVM 的热点代码探测方案、HotSpot 的即时编译、编译触发条件,以及如何从 JVM 外部观察和分析即使编译的数据和结果。

还有会了解到一些编译器的优化技术,比如:方法内联、分层编译、栈上替换、分支预测、逃逸分析、锁消除、锁膨胀...等等,这些基本上用不上,但是你知道了又显得高大上的知识点。

另外,强推R大的这个专栏:

https://www.zhihu.com/column/hllvm


image.png

专栏里面的这篇文章,宝藏:

https://zhuanlan.zhihu.com/p/25042028


image.png

image.png

JIT 对代码做了非常激进的优化。

其实回到我们的文章中,final 关键字的加上与否,表象上看是生成了两套不同的机器码,而本质上还是 final 关键字阻止了 JIT 进行激进的优化。

目录
相关文章
|
7月前
|
存储 数据安全/隐私保护 C++
【期末不挂科-C++考前速过系列P1】大二C++第1次过程考核(3道简述题&7道代码题)【解析,注释】
【期末不挂科-C++考前速过系列P1】大二C++第1次过程考核(3道简述题&7道代码题)【解析,注释】
|
存储 JavaScript 前端开发
前端阿瓜每周速记(2020 第 34 周)
毕竟不是全职写文,工作生活之余,遇到自己想写的,又不想随便水一水、或只做一个搬运工,往往需要查阅大量相关知识来吸收、总结、抽离、创新,时间上太紧,难成好作。
|
搜索推荐 小程序 Java
414天前,我以为这是编程玄学... (上)
414天前,我以为这是编程玄学... (上)
141 0
414天前,我以为这是编程玄学... (上)
|
Java
415天前,我以为这是编程玄学... (中)
415天前,我以为这是编程玄学... (中)
109 0
415天前,我以为这是编程玄学... (中)
|
并行计算 Ubuntu PyTorch
一小时肝一份文档,宠你我们是认真的
时间回到 2 月 25 日下午 6 点,我们的 Z 同学在模型部署后,推理图像的时候,输入图像预处理时间远远超出预期,竟然达到了 2 秒!Z 同学又是改函数又是 debug,还是一头雾水。可 Z 同学锲而不舍,继续钻研,最后推理成功了,但是奈何遇到了推理性能低、速度慢的问题。几经辗转,还是不得解决……
347 0
一小时肝一份文档,宠你我们是认真的
|
索引 Python
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(二)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(二)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(九)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(九)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(四)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(四)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(三)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(三)
|
索引 Python
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(一)
别整天 “学妹/前女友”了,花2小时整理了Numpy测试习题100道,做个测验吧!(一)