常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
package com.vmware.jvm;
public class Demo1 {
public static void main(String[] args) {
String a="a";
String b="b";
String c="ab";
}
}
❗️ 注意:字符串不会预先加载到StringTable中,而是当使用该字符串时才会被加载,其行为为懒惰型
反编译代码
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
public class Demo1 {
public static void main(String[] args) {
String a = "a";
String b = "b";
String c = "ab";
String d = a + b;
}
}
反编译如下
9: new #5 // class java/lang/StringBuilder //new StringBuilder()
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V 调用无参构造方法
16: aload_1 //从LocalVariableTable中加载1号槽的变量"a"
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //调用StringBuild().append("a")
20: aload_2 //从LocalVariableTable中加载2号槽的变量"b"
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//调用StringBuild().append("b")
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
//调用toString()方法 -> new String("ab");
27: astore 4 //存储到LocalVariableTable4号槽
StringBuild toString方法源码
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
相当于
new StringBuild().append("a").append("b").toString();
调用new String(“ab”)后相当于在堆中创建了一个新的字符串对象,所以
d==c //false d在堆中 c在StringTable中
javac会认为e的结果是确定的,所以进行了编译期优化
String e = "a" + "b";
反编译
29: ldc #4 // String ab
c==e //true c第一次被加载到StringTable中,e与c相同不会再次进入StringTable,e指向的地址与c的地址相同
package com.vmware.jvm;
public class Demo2 {
public static void main(String[] args) {
System.out.println();//2433
System.out.println("a");//2434
System.out.println("b");//2235
System.out.println("c");//2436
System.out.println("a");//2436
System.out.println("b");//2436
System.out.println("c");//2436
}
}
public class Demo3 {
public static void main(String[] args) {
String x="ab";
String s = new String("a") + new String("b");//实际上利用StringBuild的append方法进行拼接,"a" "b"会被先加载到串池中 StringTable=["ab","a","b"]
String s2 = s.intern();//入池失败 s2-> table."ab" s-> heap."ab" x-> table."ab"
System.out.println(s == x);//false
System.out.println(s2 == x);//true
}
}
String s2 = s.intern();//s2 -> table s-> heap
package com.vmware.jvm;
public class Demo4 {
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在编译期进行优化 s3="ab" 在StringTable中 s4=new StringBuild().append("a").append("b"),toString() 在堆中
System.out.println(s3 == s5);//true s3进入StringTable s5发现StringTable中有"ab"则指向Table中的地址
System.out.println(s3 == s6);//true s4调用intern方法后入池失败,返回池中的"ab"所以 s6即为池中的"ab"
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();//将对象new String("cd")放入池中,入池失败
//问 如果调换了后两行的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
//jdk1.8 false x2指向堆,x2调用入池方法后入池失败 x2指向堆 x1指向StringTable
//交换位置 x2入池成功,x2指向池中的cd x1指向池中的c 所以为true
//jdk1.6 首先在堆中new String("cd") 然后调用intern方法,会对堆中的对象拷贝一份入池,而x2仍然指向堆中的对象 x1发现池中有"cd"直接使用池中的对象,所以为false
}
}
为什么要将StringTable
从永久代移动到堆中?
因为永久代的内存只有当触发FullGC
的时候才会回收,而堆内存在触发MinorGC
的时候就会触发回收,由于StringTable使用比较频繁,所以JVM工程师对位置进行了移动
package com.vmware.jvm;
/**
* @apiNote StringTable垃圾回收延时
* -Xmx10m:设置堆大小为10m
* -XX:+PrintStringTableStatistics 打印StringTable信息
* -XX:+PrintGCDetails -verbose:gc 打印垃圾回收信息
*
* 初始信息
* StringTable statistics:
* Number of buckets : 60013 = 480104 bytes, avg 8.000
* Number of entries : 1711 = 41064 bytes, avg 24.000 //StirngTable中的key-value对 1711
* Number of literals : 1711 = 154040 bytes, avg 90.029 //字符串数量1711
*
* 添加10000个字符串到StringTable后
* StringTable statistics:
* Number of buckets : 60013 = 480104 bytes, avg 8.000
* Number of entries : 5845 = 140280 bytes, avg 24.000
* Number of literals : 5845 = 353352 bytes, avg 60.454
* 数量小于11711,说明StringTable内进行了垃圾回收
*/
public class Demo6 {
public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j < 10000; j++) {
String.valueOf(j).intern();
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
StringTable采用的数据结构为哈希表,所以可以通过调整主数组的大小来减小哈希碰撞的概率,进而提升效率
-XX:StringTableSize=桶个数
如果程序运行期间会产生大量字符串,并且字符串重复数量较多,可以考虑将字符串进行入池操作,减少内存开销
入池前