文章大纲
- String#intern 内存分布图
- 案例1
- 案例2
- 案例3
- String#intern 的存在意义
- String#intern 的弊端
- 案例4 垃圾回收
- OOM
- Guava Interners
接着上一篇文章 画图理解Java String的内存分布。
1 String#intern
案例1
@Test
public void demo3() {
String test = new String("ab"); // @1
String test1 = test.intern(); // @2
String test2 = "ab"; // @3
Assert.assertSame(test1, test2); // 通过
}经过上篇文章的铺垫, 我们知道 @1 这行代码执行后, 内存分布图如下所示, 此时常量池和堆中都有字符串 "ab" 存在。



案例2
@Test
public void demo3_2() {
String test = new String("ab") + new String("cd"); // @1
String test1 = test.intern(); // @2
String test2 = "abcd"; // @3
Assert.assertSame(test1, test2);// 通过
Assert.assertSame(test, test1);// 通过
Assert.assertSame(test, test2);// 通过
}同样的, 经过上篇文章的铺垫, 我们知道 @1 这行代码执行后, 字符串常量池没有"abcd", 如下图所示:

来到 @2 这行,字符串 "abcd" 调用了 intern 方法后,编译器会先去字符串常量池中查找是否有 "abcd",很明显不存在,那么:
在 jdk1.6,就将堆内存中的 "abcd" 添加到常量池中; 在 jdk1.7,那么就将指向堆内存中 "abcd" 的地址值保存到常量池中。
本文的讲解以 jdk1.7 为准。
所以此时局部变量 test1 引用的是 new String("abcd") 的地址。

来到 @3 这行, 双引号创建字符串 "abcd", 编译器同样会先去字符串常量池中查找是否有 "abcd", 此时存在,则返回常量池字符串的地址值。
所以此时,局部变量 test, test1 和 test2 都是堆内存中的 new String("abcd")。

案例3
@Test
public void demo3_3() {
String test = new String("ab") + new String("cd"); // @1
String test2 = "abcd"; // @2
String test1 = test.intern(); // @3
Assert.assertSame(test1, test2); // 通过
Assert.assertNotSame(test, test1); // 通过, test!=test1
Assert.assertNotSame(test, test2); // 通过, test!=test2
}@1 这行代码执行后的内存分布图如下,此时字符串常量池同样没有 "abcd" :



2 String#intern 存在的意义
public boolean equals(Object anObject) {
if (this == anObject) { // @1
return true;
}
if (anObject instanceof String) { // @2
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}可以明显地看到,在使用 equals 比较两个字符串变量的时候,如果他们指向的地址是同一个字符串,那么将会立刻返回 true;否者的话,就要通过 while 循环变量字符串中所有的字符!因此 @1 和 @2 的性能可以说是差异巨大。
3 String#intern 的弊端
案例4 垃圾回收
@Test
public void test_afterGC() throws InterruptedException {
String canonical = new String("5") + new String("5"); // @1
String not = new String("5") + new String("5"); // @2
assertSame(canonical, canonical.intern()); // @3
WeakReference<String> signal = new WeakReference<String>(canonical);
canonical = null; // Hint to the JIT that canonical is unreachable
System.gc(); // @4
GcFinalization.awaitClear(signal); // @5
assertSame(not, not.intern()); // @ 6
}在执行完 @3 这行代码后,内存分布图如下所示:

字符串 “55” 调用 intern 后返回的常量池中的地址是 0x0001,这和变量 not 引用的地址 0x0002 明显不是同一个对象。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>29.0-jre</version>
<scope>test</scope>
</dependency>GcFinalization 这个类正是来自 Guava 测试工具包, GcFinalization#awaitClear 默认会通过 CountDownLatch 阻塞等到 canonical 变量被回收掉。


String#intern 遇上垃圾回收之后,一切都变得不可捉摸了!
OOM
4 解决方案 Interners
private static final Interner<String> pool = Interners.newWeakInterner();
pool.intern("Order_" + orderId)X References
往期回顾

用d3动画讲解各种有趣的编程知识。