《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了
intern的作用
该方法的作用是把首次遇到的字符串加载到常量池中。
对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
测试代码
String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2);
这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。
由于JDK 6常量池位于方法区,JDK 7以后常量池位于堆中,所以用两个版本的jdk跑上面的代码就会出现神奇的事情。甚至用JDK 8来跑,也会出现你想不到的结果。
产生差异的原因是:在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。
而JDK 1.7(以及部分其他虚拟机,例如JRocki)的intern()实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到了Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。 输出false,说明调用main方法之前,肯定在字符串常量池里面已经有了这个“java”字符串了,且不是首次出现的,是在new Stringbuilder之前就存在,不符合intern首次出现的原则。而new StringBuilder在对上新建对象,当然不等。
debug
我们在main方法的第一行打上一个断点,debug运行程序后,可以看到Memory,然后过滤出String,如下:
然后双击过滤出来的java.lang.String,可以看到下图:
在这个页面我们可以继续过滤:
果然,在程序还没执行第14行之前,“java”已经出现了。
从这个结果我们可以推断出:Java标准库在JVM启动过程中加载的部分,可能里面就有类里有引用“java”字符串字面量,这个字面量被初次引用的时候就会被intern,加入到字符串常量池中去。
sun.misc.Version的init()方法加载的。
不同的java版本加载的不一定是java字符串
比如这个示例我在JDK8u212-b03上跑出来,就是两个true:
在这个版本里面,sun.misc.Version的launcher_name变成了“openjdk”:
那么根据我们之前的猜测,把程序成下面这样的,效果就是一样的了: