常量池与StringTable的关系
源代码
public class Main extends ClassLoader{
public static void main(String[] args){
String a = "a";
String b = "b";
String c = "ab";
}
}
反编译
- 字符串字面量在类加载时存入到常量池,常量池中的信息都会被加载到运行时常量池中,此时字符串还是常量池中的符号,并不是字符串对象
- ldc指令会将常量池中对应编号的符号变为字符串对象,如ldc #2 会将常量池中的“a”变成字符串对象
- StringTable是哈希结构,不可扩容
- 在ldc字符串对象时,会先去StringTable找这个字符串对象,如果没有就会从常量池中找到这个符号,并变成字符串对象
字符串拼接
public class Main extends ClassLoader{
public static void main(String[] args){
String a = "a";
String b = "b";
String ab = a+b;//new StringBuilder().append(a).append(b).toString();
String cd = "c"+"d";
}
}
- 如果拼接的字符串中出现了字符串对象(如 new String(“a”))、字符串对象的引用(如String a = “a”;中的引用a);将会创建一个new StringBuiler()通过append方法来一步步构建最终结果
- 如果拼接的字符串都是字面量,则会在通过编译期优化,直接得到最终结果
intern方法
jdk1.8
intern方法会将字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,最后将串池中的对象返回
public class Main extends ClassLoader{
public static void main(String[] args){
String a = new StringBuilder().append('a').toString();//通过StringBuilder构建的字符串对象不会放入串池中
String b = a.intern();
String c = "a";
System.out.println(b==a); // true,b是通过intern方法后从串池中得到的,之前串池中没有“a”,所以a变量指向的对象会放入串池中,然后的发哦a
System.out.println(c==a); //true,此时串池中的对象就是a
}
}
jdk1.6
intern方法会将字符串对象放入串池,如果有则不会放入,如果没有则将此对象复制一份,然后将副本放入串池,会将串池中的对象返回
public class Main extends ClassLoader{
public static void main(String[] args){
String a = new StringBuilder().append('a').toString();//通过StringBuilder构建的字符串对象不会放入串池中
String b = a.intern();
String c = "a";
System.out.println(b==a); // false,a的副本放入了串池,所以a并不是串池中的对象,a仍然在堆中
System.out.println(c==a); //false,a的副本放入了串池,所以a并不是串池中的对象,a仍然在堆中
System.out.println(b==c); // true,b是通过intern方法得到的串池中的对象,c是通过字面量指向的串池中的对象
}
}
总结
- 常量池中的字符串仅仅是符号,在第一次用到时,才会变为对象
- StringTable利用串池的机制,来避免重复创建字符串对象
- 字符串变量的拼接原理是StringBuilder构建
- 字符串常量的拼接原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
StringTable面试题
public class Main extends ClassLoader{
public static void main(String[] args){
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); // false,s3通过编译器优化,运行时当作“ab”字面量,而s4会经过StringBuilder构建一个新的字符串对象,存在堆中
System.out.println(s3==s5); // true,s3和s5都指向串池中的“ab”
System.out.println(s3==s6); // true,s4调用intern方法会返回串池中的“ab”
}
}
StringTable的位置
jdk1.8
jdk1.6
更改原因
- 永久代在Full GC时才会触发垃圾回收,Full GC在老年代垃圾回收时才会触发,所以永久代的垃圾回收概率较低;同时,Java程序在运行过程中会有大量的字符串常量进入串池,容易导致永久代内存不足
- 在1.8,堆中的StringTable只需要Minor GC就可以触发StringTable的垃圾回收
StringTable垃圾回收
虚拟机参数:
- -Xmx10m:将堆空间设置为10m
- -XX:+PrintStringTableStatistics:打印StringTable
- -XX:+PrintGCDetails -verbose:gc:打印垃圾回收细节
初始状态
public class Main extends ClassLoader{
public static void main(String[] args){
int i=0;
try {
for (int j = 0; j < 10000; j++) {
// 往串池中添加字符串
String.valueOf(j).intern();
i++;
}
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
StringTable性能调优
StringTable大小调整
- StringTable的底层结构类似于哈希表,哈希表的性能和哈希表的桶的个数有关,哈希表桶的个数较多时,哈希表的哈希碰撞较少,查找速率也较快
- 通过虚拟机参数:
-XX:StringTableSize=20000
,可以将桶个数改为两万个
为什么用StringTable
在大量字符串对象存在且重复时,重复的数据会占用大量的内存空间,使用StringTable是利用享元模式的思想,共享重复的数据,有利于节省空间