一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情。
StringTable 简介
- 也叫做串池,它是一个不能扩容的哈希表,可以避免重复创建字符串对象。
JDK 1.6
的时候,StringTable
位于运行时常量池中。JDK 1.8
的时候,StringTable
位于堆中。
StringTable 特性
- 运行时常量池中的字符串仅是符号,第一次用到时才变为对象(延迟加载)
- 字符串变量拼接的原理是
StringBuilder
(JDK 1.8
) - 字符串常量拼接的原理是编译期优化
- 可以使用
intern()
方法,尝试将字符串放入串池
代码示例
public class Demo {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
}
}
上述代码解释如下:
- 常量池中的信息,都会被加载到运行时常量池中,加载后
a
、b
、ab
都是运行时常量池中的符号,还没有变为Java
字符串对象。 - 执行到引用
a
的这行ldc #2
字节码的时候,它会将a
符号变为"a"
字符串对象。然后查看StringTable
中寻找是否存在"a"
这样的字符串,如果不存在就放进去。 String s2 = "b";
和String s3 = "ab";
这两句代码同理。- 执行到
String s4 = s1 + s2;
的时候,实际上调用的是new StringBuilder().append("a").append("b").toString()
。查看StringBuilder
的toString()
,相当于new String("ab")
,它位于堆中,所以s3 == s4
结果为false
。
编译期优化
String s3 = "ab";
与String s5 = "a" + "b";
的效果一致,都是直接查看StringTable
中是否有"ab"
,没有则存放进去。所以s3 == s5
的结果为true
。String s5 = "a" + "b";
其实是javac
在编译期的优化,因为"a"
和"b"
都是常量,不会发生改变,所以在编译期间就可以确定它们的拼接结果,所以不需要使用StringBuilder
的方式进行拼接。
字符串延迟加载
- 运行时常量池中的字符串仅是符号,第一次用到时才变为对象。
字符串的 intern 方法
JDK 1.8
的 intern()
方法
s.intern()
尝试将字符串对象s
放入串池,如果已有则不放入,如果没有则放入,最终返回串池中的对象。
代码片段 1
:
// "a" 放入串池,堆中出现 new String("a")
// "b" 放入串池,堆中出现 new String("b")
// 拼接字符串变量,堆中出现 new String("ab")
String s = new String("a") + new String("b");
// 尝试将字符串对象 s 放入串池
// 如果有则放入,如果没有则不放,最终返回串池中的对象
// 所以串池中出现 "ab",并返回它
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
// s 就是被放入串池的对象,所以为 true
System.out.println(s == "ab"); // true
代码片段 2
:
// 串池中出现 "ab"
String x = "ab";
// 串池中出现 "a",堆中出现 new String("a")
// 串池中出现 "b",堆中出现 new String("b")
// 堆中出现 new String("ab")
String s = new String("a") + new String("b");
// 尝试将字符串对象 s 放入串池
// 如果有则放入,如果没有则不放,最终返回串池中的对象
// 串池中已有 "ab",直接返回它
String s2 = s.intern();
// true:s2 和 x 都是串池中的,所以为 true
System.out.println(s2 == x);
// false:因为 x 是串池中的,s 是堆中的
System.out.println(s == x);
代码片段 3
:
// 串池中放入 "a",堆区出现 new String("a")
// 串池中放入 "b",堆区出现 new String("b")
// 堆区出现 new String("ab")
String s = new String("a") + new String("b");
// 堆区的 "ab" 放入串池,返回给 s2
String s2 = s.intern();
// 串池中的 "ab"
String x = "ab";
// true
System.out.println(s2 == x);
// true
System.out.println(s == x);
JDK 1.6
的 intern()
方法
s.intern()
尝试将字符串对象s
放入串池,如果已有则不放入,如果没有则将此对象复制一份,放入串池,最终返回串池中的对象。
团子注:关键区别就在于,
1.8
不会发生复制的行为,而1.6
串池中没有会复制一份。
代码片段 1
:
// 串池中出现 "ab"
String x = "ab";
// 串池中出现 "a",堆中出现 new String("a")
// 串池中出现 "b",堆中出现 new String("b")
// 堆中出现 new String("ab")
String s = new String("a") + new String("b");
// 尝试将 "ab" 放入串池,发现已有,返回串池的 "ab"
String s2 = s.intern();
// 都是串池的 "ab",所以 true
System.out.println(s2 == x);
// s 是堆的,x 是串池的,所以 false
System.out.println(s == x);
代码片段 2
:
// 串池中放入 "a",堆区出现 new String("a")
// 串池中放入 "b",堆区出现 new String("b")
// 堆区出现 new String("ab")
String s = new String("a") + new String("b");
// 复制 "ab",放入串池,返回给 s2
String s2 = s.intern();
// 串池中的 "ab"
String x = "ab";
// true
System.out.println(s2 == x);
// false
System.out.println(s == x);
面试题
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问:以下代码执行结果
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问:以下代码执行结果
System.out.println(x1 == x2);
// 如果调换最后两行代码的位置,结果如何
// 如果是 JDK 1.6,结果如何
false
、true
、true
、false
- 如果调换最后两行代码位置,最后一个结果为
true
- 如果是
JDK 1.6
,最后一个结果为false
StringTable 位置
JDK 1.6
时,StringTable
位于永久代JDK 1.7
及JDK 1.8
,StringTable
位于堆中- 这样的改动提高了垃圾回收效率,位于永久代的对象在
Full GC
时进行垃圾回收,触发时机比较晚,间接导致StringTable
回收效率不高。而在堆中的StringTable
会在Minor GC
时进行垃圾回收。
代码验证:
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class StringTablePos {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
结论:
- 对于
JDK 1.6
,会抛异常java.lang.OutOfMemoryError: PermGen space
,也就是永久代内存溢出。 - 对于
JDK 1.8
,设置了-Xmx10m
,表示堆最大内存为10m
,会报异常java.lang.OutOfMemoryError: GC overhead limit exceeded
。这是因为默认情况下会开启一个JVM
选项-XX:+UseGCOverheadLimit
,也就是GC
开销限制。它是一项策略,用于限制在抛出OutOfMemoryError
异常前JVM
在GC
上花费的时间。该策略默认启用,如果大于98%
的总时间花在了GC
上,而只有少于2%
的堆内存被回收时,就会抛出OutOfMemoryError
。当堆内存较小时,此功能可以用来防止应用程序长时间运行,却没有任何进展。可以使用-XX:-UseGCOverheadLimit
来禁用这一选项。 - 对于
JDK 1.8
,同时设置-Xmx10m -XX:-UseGCOverheadLimit
,会抛异常java.lang.OutOfMemoryError: Java heap space
,也就是堆空间内存溢出。
垃圾回收
StringTable
中的字符串,也是会被垃圾回收的。下面的代码通过设置不同的 j
,发现 j < 10000
时的 Number of literals
数目并不符合,说明发生了垃圾回收。
import java.util.ArrayList;
import java.util.List;
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class StringTableGC {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
虚拟机参数:
-Xmx10m
用于设置堆内存为10mb
-XX:+PrintStringTableStatistics
用于打印StringTable
的统计信息-XX:+PrintGCDetails -verbose:gc
用于打印垃圾回收的详细信息
性能调优
调整 -XX:StringTableSize=桶个数
- 如果系统中字符串常量比较多,可以适当增加
StringTable
的桶个数,避免哈希冲突。 -XX:StringTableSize=200000
,用于设置StringTable
的桶个数为200000
,默认大小为60013
。StringTable
的大小应该在1009
到2305843009213693951
。
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class StringTableSize {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
考虑是否将字符串对象入池
网上流传的段子:Twitter
为了存储用户地址,需要占用 30G
的内存空间,但是如果将这些字符串调用 intern
入池后,内存占用下降到数百兆。
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class InternDemo {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line.intern());
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}