知乎
部分内容来自于why技术
第2版57页讲了啥?
也许你根本就没看过《深入理解Java虚拟机(第2版)》这本书。但是你一定见过位于本书第57页的示例代码:
由于JDK 6常量池位于方法区,JDK 7以后常量池位于堆中,所以用两个版本的jdk跑上面的代码就会出现神奇的事情。甚至用JDK 8来跑,也会出现你想不到的结果,博主使用的是jdk11。且听我慢慢道来。
先说一下intern是干啥的。
该方法的作用是把首次遇到的字符串加载到常量池中。
再看一下intern的注释:
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
* @jls 3.10.5 String Literals
*/
public native String 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 11里面会返回fasle,上面红框框起来的部分是关键答案:
因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过。
这句话就是“坑”,已经出现过?在哪出现的,你倒是告诉我啊!我当时的内心想法和下面的老大哥是一样一样的:
这个java是关键字?????哪里来的呢?
再jdk8之前是sun.misc.version文件。再jdk11是在java.lang.VersionProps包中。
第3版注脚填坑
这个2013年(第二版出版那年)挖下的坑,在2016年10月1日,就被R大在知乎上给填上了。R大的这个回答也被作者周志明写在了2019年底出版的《深入理解Java虚拟机(第三版)》的注脚里面:
里面的RednaxelaFX就是R大,一个把虚拟机玩到极致,凭一己之力撑起了知乎java半边天的男人,后面我会详细介绍一下的。
你只要了解到一点就行:他的回答,就是权威。
在R大的这个知乎回答中,帮周志明大大填了这个坑,我强烈建议你一定要去看看,链接如下:
https://www.zhihu.com/question/51102308/answer/124441115
R大讲解是jdk8.博主使用的是jdk11.在VersionProps类中可以轻易的看出。
这个类注明了一些常用的版本控制常量。
通过查看java.lang.System类中。可以看到。
根据System类的注释我们可以知道,它是由虚拟机自动调用的。而其initPhase1方法会调用java.lang.VersionProps.init()方法。
到此就真相大白了。
Java标准库在JVM启动过程中会调用java.lang.VersionProps的init()方法。所以java.lang.VersionProps会进行类加载的操作,而类加载的初始化阶段时,会对静态常量字段进行真正的赋值操作,但是由于java.lang.VersionProps的launcher_name字段是final修饰的,所以引用的字符串“java”在准备阶段就被intern到了字符串常量池里面了。
可以在心里在默默的复习一下类加载的过程:加载、验证、准备、解析和初始化这五个阶段哦。
另外书中给出的示例代码也有一定的局限性,R大是这样说的:
其实这事情很简单:首先,这个行为必然是要针对某个具体的JDK/JRE实现来讨论的,因为Java语言规范/JVM规范/Java SE标准库的JavaDoc(也是Java SE平台规范的一部分)都没有、也不会强制指定哪个类里一定要引用“java”这个字符串常量,而且它必须是第一个使得“java”被intern的类 — 规定这个也太无聊了。
比如这个示例我在JDK8u212-b03上跑出来,就是两个true:
在这个版本里面,sun.misc.Version的launcher_name变成了“openjdk”: