23

《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzIxNTQ4MzE1NA%3D%3D&%3Bmid=2247485028&%3Bidx=1&%3Bsn=718687cfda75a96c0f7ef5dd499cd681
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

持续输出原创文章,点击蓝字关注我吧

这是why技术的第 34 篇原创文章

aANviqa.jpg!web

本周还是在家办公的一周,上面的图就是我在家的工位,和上周《 Dubbo Cluster集群那点你不知道的事 》这篇文章里面的第一张图片比起来,升级了显示器支撑臂,如果短还可以加长;用上了机械键盘,让指尖享受那一点点来自红轴的美妙反馈......

还是那句话:工欲善其事,必先利其器。在家办公,我是认真的。

图中显示器下面的两本书分别是《深入理解Java虚拟机》的第2版和第3版。也就是本文的主角。

你的手边有第2版吗?

来,翻到第57页。这里面有个“坑”,看看你当时发现了没,有没有在这页做笔记呢?

nayaiiZ.jpg!web

没有也没关系,我带你先回顾一下这一页的内容,再让你看看我三年前第一次看这书的时候做的笔记。

第2版57页讲了啥?

也许你根本就没看过《深入理解Java虚拟机(第2版)》这本书。但是你一定见过位于本书第57页的示例代码:

7n6JFje.png!web

由于JDK 6常量池位于方法区,JDK 7以后常量池位于堆中,所以用两个版本的jdk跑上面的代码就会出现神奇的事情。甚至用JDK 8来跑,也会出现你想不到的结果。且听我慢慢道来。

先说一下intern是干啥的。

该方法的作用是把 首次遇到 的字符串加载到常量池中。

再看一下intern的注释:

2iI7naf.png!web

其中标记了☆的红框翻译过来就是: 对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

回到最开始的代码中。引用第三版的描述如下:

这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。

产生差异的原因是:在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储, 返回的也是永久代里面这个字符串实例的引用 ,而由StringBuilder创建的字符串实例 在Java堆上 ,所以必然不是同一个引用,将返回false。

而JDK 1.7(以及部分其他虚拟机,例如JRocki)的intern()实现就不需要再拷贝字符串的实例到永久代了, 既然字符串常量池已经移到了Java堆中 ,那只需要在常量池里记录一下首次出现的实例引用即可, 因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个

对str2比较返回false是 因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过 ,字符串常量池中已经有它的引用了,不符合intern()方法要求“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

vquiaeu.jpg!web

挖坑不填,坑哭读者

读到这里你有没有一些不惑呢?有没有感觉到一丝丝不对呢?

我们再看看原文:

A7VVNjI.jpg!web

为什么在JDK 7里面会返回fasle,上面红框框起来的部分是关键答案:

因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过。

这句话就是“坑”,已经出现过?在哪出现的,你倒是告诉我啊!我当时的内心想法和下面的老大哥是一样一样的:

2UfInqM.gif

我第一次看这本书是在2016年,看这个地方的时候,我就百思不得其解,在哪就出现过了呀?

当时也不知道是在哪个写的似是而非的博客里面找到“java是关键字,已存在常量池中”这句“骚话”。还正正经经的抄了上去,虽然是错误的描述,虽然字是丑了点......

nqmeumj.jpg!web

你当年或者现在看的时候有这个疑惑吗?

之后我又完整的看过几次这本书,我清楚的记得,我再一次看到这里的时候我就觉得 “java是关键字,已存在常量池中”这个描述是不对的 ,所以我在后面打了一把叉,再次去找了相关资料,找到了sun.misc.version,终于解决了“在哪出现的”这个问题。

第3版注脚填坑

这个2013年(第二版出版那年)挖下的坑,在2016年10月1日,就被R大在知乎上给填上了。R大的这个回答也被作者周志明写在了2019年底出版的《深入理解Java虚拟机(第三版)》的注脚里面:

FZRvArN.jpg!web

里面的RednaxelaFX就是R大,一个把虚拟机玩到极致,凭一己之力撑起了知乎java半边天的男人,后面我会详细介绍一下的。

你只要了解到一点就行:他的回答,就是权威。

muQnqeI.gif

在R大的这个知乎回答中,帮周志明大大填了这个坑, 我强烈建议你一定要去看看,链接如下:

https://www.zhihu.com/question/51102308/answer/124441115

R大帮忙填坑

我这里只是结合R大的回答和个人的一点点经验,谈谈自己的认知。

在2016年我读这本书的时候,我才刚刚大学毕业,刚从新手村出来。当时的我对于这个问题是绝对没有任何思路的,必须直接在网上查询答案。

现在的我,略有一点经验,再次遇到这个问题,就算没看书中的描述、R大的回答,我肯定也会想到:在书中的示例里面,第二个输出false, 说明调用main方法之前,肯定在字符串常量池里面已经有了这个“java”字符串了。

怎么验证一下呢?

我们在main方法的第一行打上一个断点,debug运行程序后,可以看到Memory,然后过滤出String,如下:

yqMzYf7.png!web

然后双击过滤出来的java.lang.String,可以看到下图:

rUveAvE.png!web

在这个页面我们可以继续过滤:

MzqUVzA.png!web

果然,在程序还没执行第14行之前,“java”已经出现了。

从这个结果我们可以推断出: Java标准库在JVM启动过程中加载的部分,可能里面就有类里有引用“java”字符串字面量,这个字面量被初次引用的时候就会被intern,加入到字符串常量池中去。

而到底是哪个类导致了这个“java”字符串被intern的呢? R大主要就是回答了这个问题。

我截取一下R大最终的答案,具体探索的过程去看他的回答吧,很强很硬核:

VVZbeye.png!web

我们可以看到sum.misc.Version里面的launcher_name字段的值就是“java”:

zqYzUbF.png!web

而根据R大的回答,我们可以找到java.lang.System类:

f2e6R3J.jpg!web

根据System类的注释我们可以知道,它是由虚拟机自动调用的。而其initializeSystemClass方法会调用sun.misc.Version.init()方法。

到此就真相大白了。

Java标准库在JVM启动过程中会调用sun.misc.Version的init()方法。 所以sun.misc.Version会进行初始化的操作,而初始化时,会对静态常量字段进行真正的赋值操作,此时,sun.misc.Version的launcher_name字段所引用的字符串“java”就被intern到了字符串常量池里面了。

可以在心里在默默的复习一下类加载的过程:加载、验证、准备、解析和初始化这五个阶段哦。

另外书中给出的示例代码也有一定的局限性,R大是这样说的:

其实这事情很简单:首先,这个行为必然是要针对某个具体的JDK/JRE实现来讨论的,因为Java语言规范/JVM规范/Java SE标准库的JavaDoc(也是Java SE平台规范的一部分)都没有、也不会强制指定哪个类里一定要引用“java”这个字符串常量,而且它必须是第一个使得“java”被intern的类 --- 规定这个也太无聊了。

比如这个示例我在JDK8u212-b03上跑出来,就是两个true:

nq6Vzq2.png!web

在这个版本里面,sun.misc.Version的launcher_name变成了“openjdk”:

rQFFbm7.png!web

那么根据我们之前的猜测,把程序成下面这样的,效果就是一样的了:

Yn6RjuN.png!web

万变不离其宗,现在你知道为什么这里用openjdk返回也是false了吧。

知其然,还要知其所以然。

qeemyqM.gif

R大与周志明之间的“爱恨情仇”

R大是谁?

eyyINr2.gif

我先上一张《深入理解java虚拟机(第二版)》背面的一张图吧,R大给这本书写过推荐语:

vmANZrF.jpg!web

莫枢(RednaxelaFx)Oracle HotSpot VM编译器团队工程师。(现在他已经不在Oracle了。据网上公开资料,R大是前阿里巴巴技术专家,前Oracle JVM核心开发,前 Azul核心开发,现就职于Databricks )

再看一下他的知乎主页:

https://www.zhihu.com/people/rednaxelafx/answers

uYBVFnY.jpg!web

你去知乎上只搜RednaxelaFX(甚至直接用搜索引擎搜索),就能搜到很多结果,我顺便截取一个片段。

在【有哪些顶级水平的中国程序员?】这个话题下,有一个回答只是@一下R大的ID,没有多说一个字,就获得了258个赞,评论中也满是赞美的语言,干货多,就是他的特点:

nmIFnaj.jpg!web

他与《深入理解Java虚拟机》的作者周志明大大,在2010年到2011年间,在iteye上已经有过多次深度交流,比如下面的吐槽:

zMfiY3n.png!web

比如下面的调侃:

2YZjeui.png!web

玩归玩,闹归闹,周志明也直言阅读了R大的很多文章,受益良多:

Z3Mfymy.jpg!web

并且在书里的致谢章节专门谢谢了R大:

Mb2IF3i.jpg!web

说这么多,我想要表达的观点其实就是一个:

R大是一个宝藏啊,他乐于分享和交流,凭借一己之力推动了国内jvm的学习和研究,如果你想要了解虚拟机、编译原理和编程语言方面的相关知识,他是一个你绕不过的人。他值得被更多的程序员知道。

如果你之前不知道,但看了我这篇文章后知道了他,我的目的就达到了。

他在知乎上认认真真码字,用心的对待每一个回答,他是一个"码"宗强者,恐怖如斯,但是从他的各种回答、博客文章中,你可以感觉到谦逊、细致、系统、耐心、专业、严谨.....就像一个评论说的:

在技术圈日益浮躁的今天,感觉他就是主席所说的那种: 一个纯粹的人,一个有道德的人,一个脱离了低级趣味的人,一个有益于人民的人。

我们做程序的,要向他学习,向他致敬。

RRF3Qvn.gif

最后再附上一个R大的资料合集链接吧,全是宝藏,待你去发掘:

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

最后说一句(求个关注)

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。

感谢您的阅读, 我坚持原创 ,十分欢迎并感谢您的关注。

以上。

原创不易,欢迎转发, 求个关注 ,文末右下角赏个"在看"吧。

aqqiyir.jpg!web

持续输出

长按识别关注


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK