为了您更好的理解本篇文章,请先查阅
https://blog.csdn.net/define_us/article/details/78252783 中的JVM分代管理策略一节。
JDK6
- JDK的String常量池存在于Perm区,和JAVA Heap区是完全不同的两个区域。
- 通过字符串常量生成的String对象存在在永久代上,
- 而通过new产生的对象,分配在JAVA HEAP上。
intern会查看是否有String常量池的值等于当前字符串,如果没有,把在常量池新建该值的字符串对象。然后把常量池中的字符串对象返回。
public static void main(String[] args) {
//指向常量池中的同一个字符串对象
String s5 = "111";
String s6 = "111";
System.out.println(s5 == s6);//true
//s8是intern后返回的常量池中的字符串对象
String s7 = "111";
String s8 = new String("111").intern();
System.out.println(s7 == s8);//true
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);//false s是堆中的对象
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//false 同理,s3是堆中的对象
}
- JDK6有一个硬伤,就是subString的内存泄漏问题
如下代码:
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3) + "";
str = null;
运行结束后str内部的char数组不会被回收,因为substring是通过调整边界实现的。在str被置为null后,sub内部仍然引用者char数组。
public String substring(int var1, int var2) {
return var1 == 0 && var2 == this.count ?
this : new String(this.offset + var1, var2 - var1, this.value);//这里复用了当前字符串的value数组。
}
String(int var1, int var2, char[] var3) {
this.value = var3;
this.offset = var1;
this.count = var2;
}
采用如下方式就可以解决该问题。
String str = "abcdefghijklmnopqrst";
String sub = str.substring(1, 3) + "";
str = null;
JDK7
public static void main(String[] args) {
String s3 = new String("1") + new String("1");
String s5 = s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
System.out.println(s3 == s5);
}
}
上述的代码,在JDK6中都是false,但是在JDK7中则都是true。
jdk1.6中,s3.intern()运行时,首先去常量池查找,发现没有该常量,则在常量池中开辟空间存储"11",返回其地址(S5),第三行中,s4通过查找常量池也指向了常量池存储的对象。所以S3是堆中的对象,S4,S5是常量池中对象。
jdk1.7中,由于常量池放到了堆空间中,所以在s3.intern()运行时,发现常量池(stringTable)没有常量,则添加常量并使其指向s3堆中的地址,返回堆中的地址(注意这里也没有使用该返回值) 。这时s4通过查找常量池中的常量,找到了堆中的地址并指向它,所以S3 = S4;
intern:
a.intern()会在字符串池中查找是否有一个字符串引用所指向的对象的值等于a的值,如果有就直接返回池中的引用;
如果没有,就把a指向的对象的引用放入池中,在返回该引用。
如果改变下顺序,则在JDK7中的结果就都是false。这是因为直接使用字符串常生成的String也会被分配在常量池子
public static void main(String[] args) {
String s4 = "11";
String s3 = new String("1") + new String("1");
String s5 = s3.intern();
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//false
}
故那件事理解,字符串常量池中存储是引用,而非对象。
JDK8
在JDK8中运行上面的两个例子,上述代码和JDK7中的结果相同。唯一有改变的split的实现
public static void main(String[] args) {
String str = "ABC";
// 测试String#split()方法
String[] elems = str.split("");
System.out.println(Arrays.toString(elems));
// 结果:
// Java 7 -> [, A, B, C]
// Java 8 -> [A, B, C]
}
然而故事并没有结束
update20 增加了UseStringDeduplication具体的实现大致是JVM会记录char[]的weak reference及hash value,当找到一个hash code相同的String时,就会挨个char进行比较,当所有都match,那么其中一个String就会修改指针指向另一个String的char[],这样前者的char[]就可以被回收选项。开启之后,所有相同内容的字符串公用相同的char[]底层对象。
With this feature,
if you have 1000 distinct String objects, all with the same content "abc", JVM could make them share the same char[] internally.
However, you still have 1000 distinct String objects.
With intern(), you will have just one String object. So if memory saving is your concern, intern() would be better. It'll save space, as well as GC time.
但是开启了UseStringDeduplication(前提是使用G1)这个选项之后,可能会大幅度提高单次GC停顿时间。所以现在不推荐使用这个。
具体的实现大致是JVM会记录char[]的weak reference及hash value,当找到一个hash code相同的String时,就会挨个char进行比较,当所有都match,那么其中一个String就会修改指针指向另一个String的char[],这样前者的char[]就可以被回收。