最近在看周志明的《深入理解Java虚拟机》,第二章有一段关于运行时常量池的代码,我先贴在下面
class RuntimeConstantPoolOOM {
public void main(String[] args) {
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);
}
}
书中给了这段代码的解释,在JDK7以前,会输出
false
false
而在JDK7及之后,会输出
true
false
在此之前我只知道字符串在Java中的保存和常量池有关,intern()方法大致是一个返回常量池中字符串所在地址的方法。我明白这肯定是一个很浅显的解释,于是我继续看书:
这段代码在JDK6中运行,会得到两个false,而在JDK7中运行,会得到一个true和一个false。产生差异的原因是,在JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。
而JDK7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为"java"(在sun.misc.Version中被加载)这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。
看完这两段话我真的是脑袋一头雾水了,完全没办法搞明白上面的代码究竟为什么会输出那样的结果。于是我开始漫长的网络搜索,看了各种各样的帖子,但是这些帖子基本上都是在讨论下面几个事情:
1.String s = ""hello"; 和 String s = new String("hello"); 有什么区别?
2.String s1 = new String("xyz"); 这行代码创建了几个String对象?
3.对intern()方法的一些片面解释 。。。。等等
有些从内存角度讲解的帖子也是各相矛盾,不知道应该相信谁。
如果你也搜索了很多相关的帖子,应该大致也知道上面这些问题的“答案”,可是一旦你希望再了解的深入一点,你会发现很多帖子的说法在某些点上是互相矛盾的,最为典型的就是,我非常想搞清楚究竟字符串的实例对象能不能只创建在堆中,无数的帖子都没有给我一个满意的答案。
直到我看到RednaxelaFX大神的这一篇文章:
请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧-pudn.com
这篇文章中提到:
问题:
String s = new String("xyz"); 创建了几个String Object?运行时的类加载过程与实际执行某个代码片段,两者必须分开讨论才有那么点意义。
为了执行问题中的代码片段,其所在的类必然要先被加载,而且同一个类最多只会被加载一次(要注意对JVM来说“同一个类”并不是类的全限定名相同就足够了,而是<类全限定名, 定义类加载器>一对都相同才行)。
根据上文引用的规范的内容,符合规范的JVM实现应该在类加载的过程中创建并驻留一个String实例作为常量来对应"xyz"字面量;具体是在类加载的resolve阶段进行的。这个常量是全局共享的,只在先前尚未有内容相同的字符串驻留过的前提下才需要创建新的String实例。
等到真正执行原问题中的代码片段时,JVM需要执行的字节码类似这样:0: new #2; //class java/lang/String 3: dup 4: ldc #3; //String xyz 6: invokesp