JDK 6及以前,字符串常量池位于永久代(方法区)中;JDK 7开始,字符串常量池从永久代移动到了堆中。事实上, JDK 6和JDK 7及以后的版本在String.intern()
的具体实现上有所差异,因此对于同样的代码可能会导致不同的结果。
在详细分析intern()
的原理之前,先引入一个“小工具”,它能帮我们打印对象的内存地址,这个工具的代码参考了[2],代码如下:
public class Test {
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static long addressOf(Object o)
throws Exception {
Object[] array = new Object[]{o};
long baseOffset = unsafe.arrayBaseOffset(Object[].class);
int addressSize = unsafe.addressSize();
long objectAddress;
switch (addressSize) {
case 4:
objectAddress = unsafe.getInt(array, baseOffset);
break;
case 8:
objectAddress = unsafe.getLong(array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
}
return (objectAddress);
}
}
提出问题
考虑《深入理解Java虚拟机》中的一个例子:
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, false
;对于JDK 7,上述代码会打印出true, false
,这是为什么呢?别急,我们先了解一下intern()
是怎么工作的。
JDK 6和 JDK 7的intern()
原理分析
考虑下面一段代码:
String str = new String("Hel") + new String("lo"); // 1
System.out.println(addressOf(str)); // 2
System.out.println(addressOf(str.intern())); // 3
对于JDK 6,上述代码会打印出两个不同的地址;对于JDK 7 上述代码会打印出两个相同的地址。这实际上是因为,JDK 6和JDK 7底层在实现intern()
时有所不同。
JDK 6
在JDK 6中,上述程序的内存情况如下图所示:
-
第1行代码:
"Hel"
和"lo"
是字面量,会被放出永久代的字符串常量池,然后str
指向一个堆中的String
类型对象 -
第3行代码: 调用
intern()
时,首先会检查常量池中有没有值等于"Hello"
的对象,发现没有,那么会在常量池中创建一个值为"Hello"
的对象,并返回该对象的指针。
JDK 7
在JDK 7中,上述程序的内存情况如下图所示:
- 第1行代码:同上
-
第3行代码:调用
intern()
时,首先会检查常量池中有没有值等于"Hello"
的对象,发现没有,那么会在常量池中创建一个值为"Hello"
的对象,但是返回的是堆中的字符串对象的地址。所以上述程序打印出来的两个地址是相同的。
根据参考的资料[1]和[3],JDK 6和JDK 7的intern()
的大致原理就是如上面所说的那样,要想了解具体是怎么实现的,还是要阅读JDK底层的源码。
解决问题
了解了intern()
的大致工作原理之后,再回到一开始的问题:
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 7做一下分析,JDK 6为什么得到那样的输出请读者自行研究一下:
-
第1行代码:
"计算机"
和"软件"
会被放入字符串常量池,在堆中创建一个String
对象 -
第2行代码: 执行
intern()
,发现字符串常量池没有值为"计算机软件"
的对象,于是在字符串常量池创建String
对象,然后返回对str1
的引用,因此str1.intern() == str1
的值为true
。(事实上,上一小节我们也验证了,str1.intern()
和str1
指向的内存地址是相同的) -
第3行代码:
"ja"
和"va"
会被放入字符串常量池,在堆中创建一个String
对象 -
第4行代码: 执行
intern()
,发现字符串常量池中有值为"java"
的字符串对象,直接返回对常量池中这个对象的引用,因此str2.intern()
指向的是字符串常量池中的对象,而str2
指向的是堆中的对象,所以不相等。
至于为什么执行第4行代码时常量池中已经存在字符串"java"
了, 这涉及类加载的细节,下面是《深入理解Java虚拟机》中的解释:
它是在加载sun.misc.Version这个类的时候进入常量池的。 本书第2版
并未解释java这个字符串此前是哪里出现的, 所以被批评“挖坑不填
了”(无奈地摊手) 。 如读者感兴趣是如何找出来的, 可参考RednaxelaFX的知乎回答
(https://www.zhihu.com/questio...)
参考
[1] https://stackoverflow.com/que...
[2] https://stackoverflow.com/que...
[3] https://stackoverflow.com/a/7...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。