继续挖
继续往下挖的线索,其实已经在前面出现过了:
通过这个链接,我们可以来到这个地方:
瞟一眼我框起来的代码,你会发现这里抛出的问题其实又是和前面是一样。
我为什么又要把它拿出来说一次呢?
因为它只是一个跳板而已,我想引出这下面的一个回答:
这个回答说里面有两个吸引到我注意的地方。
第一个就是这个回答本身,他说:这是该类的作者 Doug Lea 喜欢使用的一种极端优化。这里有个超链接,你可以去看看,能很好地回答你的问题。
这里面提到的这个超链接,很有故事:
http://mail.openjdk.java.net/pipermail/core-libs-dev/2010-May/004165.html
但是在说这个故事之前,我想先说说这个回答下面的评论,也就是我框起来的部分。
这个评论观点鲜明的说:需要着重强调“极端”!这不是每个人都应该效仿的、通用的、良好的写法。
凭借我在 stackoverflow 混了这么几年的自觉,这里藏龙卧虎,一般来说 说话底气这么足的,都是大佬。
于是我点了他的名字,去看了一眼,果然是大佬:
这哥们是谷歌的,参与了很多项目,其中就有我们非常熟悉的 Guava,而且不是普通开发者,而是 lead developer。同时也参与了 Google 的 Java 风格指南编写。
所以他说的话还是很有分量的,得听。
然后,我们去到那个很有故事的超链接。
这个超链接里面是一个叫做 Ulf Zibis 的哥们提出的问题:
Ulf 同学的提问里面提到说:在 String 类中,我经常看到成员变量被复制到局部变量。我在想,为什么要做这样的缓存呢,就这么不信任 JVM 吗,有没有人能帮我解答一下?
Ulf 同学的问题和我们文章中的问题也是一样的,而他这个问题提出的时间是 2010 年,应该是我能找到的关于这个问题最早出现的地方。
所以你要记住,下面的这些邮件中的对话,已经是距今 12 年前的对话了。
在对话中,针对这个问题,有比较官方的回答:
来自 SUN 公司的 JDK 并发大师,就问你怕不怕。
他说:这是一种由 Doug Lea 发起的编码风格。这是一种极端的优化,可能没有必要。你可以期待 JIT 做出同样的优化。但是,对于这类非常底层的代码来说,写出的代码更接近于机器码也是一件很 nice 的事情。
关于这个问题,这几个人有来有回的讨论了几个回合:
主要再看看这个叫做 Osvaldo 对线 Martin 的邮件:
https://mail.openjdk.java.net/pipermail/core-libs-dev/2010-May/004168.html
Osvaldo 老哥写了这么多内容,主要是想喷 Martin 的这句话:这是一种极端的优化,可能没有必要。你可以期待 JIT 做出同样的优化。
他说他做了实验,得出的结论是这个优化对以 Server 模式运行的 Hotspot 来说没有什么区别,但对于 Client 模式运行的 Hotspot 来说却非常重要。在他的测试案例中,这种写法带来了 6% 的性能提升。
然后他说他现在包括未来几年写的代码应该都会运行在以 Client 模式运行的 Hotspot 中。所以请不要乱动 Doug 特意写的这种优化代码,我谢谢你全家。
同时他还提到了 JavaME、JavaFX Mobile&TV,让我不得不再次提醒你:这段对话发生在 12 年前,他提到的这些技术,在我的眼里已经是过眼云烟了,只听过,没见过。
哦,也不能算没见过,毕竟当年读初中的时候还玩过 JavaME 写的游戏。
就在 Osvaldo 老哥言辞比较激烈的情况下,Martin 还是做出了积极的回应:
Martin 说谢谢你的测试,我也已经把这种编码风格融合到我的代码里面了,但是我一直在纠结的事情是是否也要推动大家这样去做。因为我觉得我们可以在 JIT 层面优化这个事情。
接下来,最后一封邮件,来自一位叫做 David Holmes 的老哥。
巧了,这位老哥的名字在《Java并发编程实战》一书里面,也可以找到。
人家就是作者,我介绍他的意思就是想表达他的话也是很有分量的:
因为他的这一封邮件,算是给这个问题做了一个最终的回答。
我带着自己的理解,用我话来给你全文翻译一下,他是这样说的:
我已经把这个问题转给了 hotspot-compiler-dev,让他们来跟进一下。
我知道当时 Doug 这样写的原因是因为当时的编译器并没有相应的优化,所以他这样写了一下,帮助编译器进行优化了一波。但是,我认为这个问题至少在 C2 阶段早就已经解决了。如果是 C1 没有解决这个问题的话,我觉得是需要解决一下的。
最后针对这种写法,我的建议是:在 Java 层面上不应该按照这样的方式去敲代码。
There should not be a need to code this way at the Java-level.
至此,问题就梳理的很清楚了。
首先结论是不建议使用这样的写法。
其次,Doug 当年这样写确实是一种优化,但是随着编译器的发展,这种优化下沉到编译器层面了,它帮我们做了。
最后,如果你不明白前面提到的 C1,C2 的话,那我换个说法。
C1 其实就是 Client Compiler,即客户端编译器,特点是编译时间较短但输出代码优化程度较低。
C2 其实就是 Server Compiler,即服务端编译器,特点是编译耗时长但输出代码优化质量也更高。
前面那个 Osvaldo 说他主要是用客户端编译器,也就是 C1。所以后面的 David Holmes 才一直在说 C2 是优化了这个问题的,C1 如果没有的话可以跟进一下,巴拉巴拉巴拉的...
关于 C2 的话,简单提一下,记得住就记,记不住也没关系,这玩意一般面试也不考。
大家常常提到的 JVM 帮我们做的很多“激进”的为了提升性能的优化,比如内联、快慢速路径分析、窥孔优化,都是 C2 搞的事情。
另外在 JDK 10 的时候呢,又推出了 Graal 编译器,其目的是为了替代 C2。
至于为什么要替换 C2,额,原因之一你可以看这个链接...
C2 的历史已经非常长了,可以追溯到 Cliff Click 大神读博士期间的作品,这个由 C++ 写成的编译器尽管目前依然效果拔群,但已经复杂到连 Cliff Click 本人都不愿意继续维护的程度。
你看前面我说的 C1、C1 的特点,刚好是互补的。
所以为了在程序启动、响应速度和程序运行效率之间找到一个平衡点,在 JDK 6 之后,JVM 又支持了一种叫做分层编译的模式。
也是为什么大家会说:“Java 代码运行起来会越来越快、Java 代码需要预热”的根本原因和理论支撑。
在这里,我引用《深入理解Java虚拟机HotSpot》一书中 7.2.1 小节[分层编译]的内容,让大家简单了解一下这是个啥玩意。
首先,我们可以使用 -XX:+TieredCompilation
开启分层编译,它额外引入了四个编译层级。
- 第 0 级:解释执行。
- 第 1 级:C1 编译,开启所有优化(不带 Profiling)。Profiling 即剖析。
- 第 2 级:C1 编译,带调用计数和回边计数的 Profiling 信息(受限 Profiling).
- 第 3 级:C1 编译,带所有Profiling信息(完全Profiling).
- 第 4 级:C2 编译。
常见的分层编译层级转换路径如下图所示:
- 0→3→4:常见层级转换。用 C1 完全编译,如果后续方法执行足够频繁再转入 4 级。
- 0→2→3→4:C2 编译器繁忙。先以 2 级快速编译,等收集到足够的 Profiling 信息后再转为3级,最终当 C2 不再繁忙时再转到 4 级。
- 0→3→1/0→2→1:2/3级编译后因为方法不太重要转为 1 级。如果 C2 无法编译也会转到 1 级。
- 0→(3→2)→4:C1 编译器繁忙,编译任务既可以等待 C1 也可以快速转到 2 级,然后由 2 级转向 4 级。
如果你之前不知道分层编译这回事,没关系,现在有这样的一个概念就行了。
再说一次,面试不会考的,放心。
好了,恭喜你看到这里了。回想全文,你学到了什么东西呢?
是的,除了一个没啥卵用的知识点外,什么都没有学到。