《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了
这是why技术的第34篇原创文章
本周还是在家办公的一周,上面的图就是我在家的工位,和上周《Dubbo Cluster集群那点你不知道的事》这篇文章里面的第一张图片比起来,升级了显示器支撑臂,如果短还可以加长;用上了机械键盘,让指尖享受那一点点来自红轴的美妙反馈......
还是那句话:工欲善其事,必先利其器。在家办公,我是认真的。
图中显示器下面的两本书分别是《深入理解Java虚拟机》的第2版和第3版。也就是本文的主角。
你的手边有第2版吗?
来,翻到第57页。这里面有个“坑”,看看你当时发现了没,有没有在这页做笔记呢?
没有也没关系,我带你先回顾一下这一页的内容,再让你看看我三年前第一次看这书的时候做的笔记。
第2版57页讲了啥?
也许你根本就没看过《深入理解Java虚拟机(第2版)》这本书。但是你一定见过位于本书第57页的示例代码:
由于JDK 6常量池位于方法区,JDK 7以后常量池位于堆中,所以用两个版本的jdk跑上面的代码就会出现神奇的事情。甚至用JDK 8来跑,也会出现你想不到的结果。且听我慢慢道来。
先说一下intern是干啥的。
该方法的作用是把首次遇到的字符串加载到常量池中。
再看一下intern的注释:
其中标记了☆的红框翻译过来就是:对于任意两个字符串 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。
挖坑不填,坑哭读者
读到这里你有没有一些不惑呢?有没有感觉到一丝丝不对呢?
我们再看看原文:
为什么在JDK 7里面会返回fasle,上面红框框起来的部分是关键答案:
因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过。
这句话就是“坑”,已经出现过?在哪出现的,你倒是告诉我啊!我当时的内心想法和下面的老大哥是一样一样的:
我第一次看这本书是在2016年,看这个地方的时候,我就百思不得其解,在哪就出现过了呀?
当时也不知道是在哪个写的似是而非的博客里面找到“java是关键字,已存在常量池中”这句“骚话”。还正正经经的抄了上去,虽然是错误的描述,虽然字是丑了点......
你当年或者现在看的时候有这个疑惑吗?
之后我又完整的看过几次这本书,我清楚的记得,我再一次看到这里的时候我就觉得**“java是关键字,已存在常量池中”这个描述是不对的**,所以我在后面打了一把叉,再次去找了相关资料,找到了sun.misc.version,终于解决了“在哪出现的”这个问题。
第3版注脚填坑
这个2013年(第二版出版那年)挖下的坑,在2016年10月1日,就被R大在知乎上给填上了。R大的这个回答也被作者周志明写在了2019年底出版的《深入理解Java虚拟机(第三版)》的注脚里面:
里面的RednaxelaFX就是R大,一个把虚拟机玩到极致,凭一己之力撑起了知乎java半边天的男人,后面我会详细介绍一下的。
你只要了解到一点就行:他的回答,就是权威。
在R大的这个知乎回答中,帮周志明大大填了这个坑,我强烈建议你一定要去看看,链接如下:
https://www.zhihu.com/question/51102308/answer/124441115
R大帮忙填坑
我这里只是结合R大的回答和个人的一点点经验,谈谈自己的认知。
在2016年我读这本书的时候,我才刚刚大学毕业,刚从新手村出来。当时的我对于这个问题是绝对没有任何思路的,必须直接在网上查询答案。
现在的我,略有一点经验,再次遇到这个问题,就算没看书中的描述、R大的回答,我肯定也会想到:在书中的示例里面,第二个输出false,说明调用main方法之前,肯定在字符串常量池里面已经有了这个“java”字符串了。
怎么验证一下呢?
我们在main方法的第一行打上一个断点,debug运行程序后,可以看到Memory,然后过滤出String,如下:
然后双击过滤出来的java.lang.String,可以看到下图:
在这个页面我们可以继续过滤:
果然,在程序还没执行第14行之前,“java”已经出现了。
从这个结果我们可以推断出:Java标准库在JVM启动过程中加载的部分,可能里面就有类里有引用“java”字符串字面量,这个字面量被初次引用的时候就会被intern,加入到字符串常量池中去。
**而到底是哪个类导致了这个“java”字符串被intern的呢?**R大主要就是回答了这个问题。
我截取一下R大最终的答案,具体探索的过程去看他的回答吧,很强很硬核:
我们可以看到sum.misc.Version里面的launcher_name字段的值就是“java”:
而根据R大的回答,我们可以找到java.lang.System类:
根据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:
在这个版本里面,sun.misc.Version的launcher_name变成了“openjdk”:
那么根据我们之前的猜测,把程序成下面这样的,效果就是一样的了:
万变不离其宗,现在你知道为什么这里用openjdk返回也是false了吧。
知其然,还要知其所以然。
R大与周志明之间的“爱恨情仇”
R大是谁?
我先上一张《深入理解java虚拟机(第二版)》背面的一张图吧,R大给这本书写过推荐语:
莫枢(RednaxelaFx)Oracle HotSpot VM编译器团队工程师。(现在他已经不在Oracle了。据网上公开资料,R大是前阿里巴巴技术专家,前Oracle JVM核心开发,前Azul核心开发,现就职于Databricks)
再看一下他的知乎主页:https://www.zhihu.com/people/rednaxelafx/answers
你去知乎上只搜RednaxelaFX(甚至直接用搜索引擎搜索),就能搜到很多结果,我随便截取一个片段。
在【有哪些顶级水平的中国程序员?】这个话题下,有一个回答只是@一下R大的ID,没有多说一个字,就获得了258个赞,评论中也满是赞美的语言,干货多,就是他的特点:
他与《深入理解Java虚拟机》的作者周志明大大,在2010年到2011年间,在iteye上已经有过多次深度交流,比如下面的吐槽:
比如下面的调侃:
玩归玩,闹归闹,周志明也直言阅读了R大的很多文章,受益良多:
并且在书里的致谢章节专门谢谢了R大:
说这么多,我想要表达的观点其实就是一个:
R大是一个宝藏啊,他乐于分享和交流,凭借一己之力推动了国内jvm的学习和研究,如果你想要了解虚拟机、编译原理和编程语言方面的相关知识,他是一个你绕不过的人。他值得被更多的程序员知道。
如果你之前不知道,但看了我这篇文章后知道了他,我的目的就达到了。
他在知乎上认认真真码字,用心的对待每一个回答,他是一个"码"宗强者,恐怖如斯,但是从他的各种回答、博客文章中,你可以感觉到谦逊、细致、系统、耐心、专业、严谨.....就像一个评论说的:
在技术圈日益浮躁的今天,感觉他就是主席所说的那种:一个纯粹的人,一个有道德的人,一个脱离了低级趣味的人,一个有益于人民的人。
我们做程序的,要向他学习,向他致敬。
最后再附上一个R大的资料合集链接吧,全是宝藏,待你去发掘:https://zhuanlan.zhihu.com/p/25042028
最后说一句
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
以上。
欢迎关注公众号【why技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。
《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了的更多相关文章
- 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)请自取
最近在读,附上网盘链接 复制这段内容后打开百度网盘手机App,操作更方便哦 链接:https://pan.baidu.com/s/1U6yFeZxz9uD6sSiu-Br06g 提取码:3Wt4
- 《深入理解Java虚拟机》第 3 版里面到底多了哪些知识点?本文竟然得到了本书作者的认可!
这是why的第 47 篇原创文章 荒腔走板 大家好,我是 why.老规矩,先是简短的荒腔走板聊聊生活. 上面的图是前几天拍的,那天晚上下班后,刚刚走进小区就看到了这一轮弯月和旁边那一颗特别特别亮的星星 ...
- 《深入理解java虚拟机》学习笔记之虚拟机即时编译详解
郑重声明:本片博客是学习<深入理解java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块 ...
- 深入理解Java虚拟机(字节码执行引擎)
深入理解Java虚拟机(字节码执行引擎) 本文首发于微信公众号:BaronTalk 执行引擎是 Java 虚拟机最核心的组成部分之一.「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力, ...
- 深入理解Java虚拟机(程序编译与代码优化)
文章首发于微信公众号:BaronTalk,欢迎关注! 对于性能和效率的追求一直是程序开发中永恒不变的宗旨,除了我们自己在编码过程中要充分考虑代码的性能和效率,虚拟机在编译阶段也会对代码进行优化.本文就 ...
- 深入理解Java虚拟机(类文件结构)
深入理解Java虚拟机(类文件结构) 欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 之前在阅读 ASM 文档时,对于已编译类的结构.方法描述符.访问标志.ACC_PUBLIC.ACC_P ...
- 深入理解Java虚拟机(自动内存管理机制)
文章首发于公众号:BaronTalk 书籍真的是常读常新,古人说「书读百遍其义自见」还是很有道理的.周志明老师的这本<深入理解 Java 虚拟机>我细读了不下三遍,每一次阅读都有新的收获, ...
- 深入理解Java虚拟机(类加载机制)
文章首发于微信公众号:BaronTalk 上一篇文章我们介绍了「类文件结构」,这一篇我们来看看虚拟机是如何加载类的. 我们的源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行.虚拟机 ...
- 深入理解Java虚拟机内存模型
前言 本文中部分内容引用至<深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)>第12章,如果有兴趣可自行深入阅读,文末放有书籍PDF版本连接. 一.物理机中的并发 物理机遇到的并 ...
随机推荐
- Map2Shp软件字符编码解决方案——彻底杜绝Shape格式乱码
在使用Shape文件时,如果里面有中文属性信息时,经常会遇到属性信息变为乱码.尤其是ArcGIS10.2.1之后,Esri改变了软件的默认字符编码规则,打开之前保存的Shapefile文件,总会不时遇 ...
- Java知识体系框架
前言:自从出生,每个人都是一个学习者或探索者.永远保持一颗谦逊的心态,遵循一定的方法和规范,去学习和实践,永远记得走走停停,多回头看看自己走过的路,温故而知新,也能更好地指导未来的路怎么走(同样,本篇 ...
- MySQL 行列相互转换
行列相互转换 /*创建表*/ CREATE TABLE ic ( NAME ), Product ), amount INT ); INSERT INTO ic VALUES (), (), (), ...
- 为什么 K8s 在阿里能成功?| 问底中国 IT 技术演进
作者: 曾凡松 阿里云云原生应用平台高级技术专家 张振 阿里云云原生应用平台高级技术专家 导读:本文描述了阿里巴巴在容器管理领域的技术演进历程,解读了为什么 K8s 最终能够大获成功的原因,以及到今年 ...
- ES.01.Elasticsearch安装配置
Windows版 提纲: 1. 安装Elasticsearch 1.1. 下载Elasticsearch: https://www.elastic.co/cn/downloads/elastics ...
- restframework 认证、权限、频率组件
一.认证 1.表的关系 class User(models.Model): name = models.CharField(max_length=32) pwd = models.CharField( ...
- GStreamer基础教程13 - 调试Pipeline
摘要 在很多情况下,我们需要对GStreamer创建的Pipeline进行调试,来了解其运行机制以解决所遇到的问题.为此,GStreamer提供了相应的调试机制,方便我们快速定位问题. 查看调试日志 ...
- Linux中两个重要的基础服务
本文服务器基于centos7,客户端Windows10 FTP FTP(File Transfer Protocol),文件传输协议,是一个比较古老的基于TCP,用于不同计算机间传递文件的协议. 安装 ...
- C语言进阶——全局变量
全局变量 ·定义在函数外面的变量是全局变量 ·全局变量具有全局的生存期和作用域 ·它们与任何函数都无关 ·在任何函数内部都可以使用它们 全局变量初始化 ·没有做初始化的全局变量会得到0值 ·指针会得到 ...
- Hystrix 监控数据聚合 Turbine【Finchley 版】
原文地址:https://windmt.com/2018/04/17/spring-cloud-6-turbine/ 上一篇我们介绍了使用 Hystrix Dashboard 来展示 Hystrix ...