先看几道面试题:
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();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
以上代码的运行结果如何,如果尚有疑惑,请看下面的分析
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
}
反编译Demo1.class(之前需要运行将.java文件生成.class文件)
F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\Demo1.class
常量池中信息
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // Demo1
#6 = Class #29 // java/lang/Object
......
main方法字节码信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
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
LineNumberTable:
line 9: 0
line 10: 3
line 11: 6
line 14: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
可以看到ldc #2,也就是到常量池中2号位置加载信息,这个例子中2号位置对应字符串对象String a(注释中),然后astore_1也就是把加载好的a字符串对象存入1号的局部变量。低下的 LocalVariableTable:也就是main方法栈帧运行时局部变量表中的变量,编号是1.同理,ldc #3,String b存入到局部变量表中2号位置去,ab字符串存入到3中去。
看完上面代码之后,我们要理清楚常量池与串池的关系。
编译后常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
当具体执行到引用它的代码上,就会变成java字符串对象
ldc #2 会把 a 符号变为 “a” 字符串对象
在变为a字符串对象之后,还要准备好一块空间,即StringTable字符串常量池,刚开始里面是空的,将a变为字符串对象之后,就会去StringTable中找,看有没有相同的key,在数据结构上是一个hash表,长度固定,不能扩容。如果没有,就会把a放入串池。
只有执行到用到它的代码,才开始创建字符串对象,放入到常量池中
它们在行为上是懒惰的
如果串池中有了,就会使用串池中的对象
ldc #3 会把 b 符号变为 “b” 字符串对象
ldc #4 会把 ab 符号变为 “ab” 字符串对象
最终
StringTable [ “a”, “b” ,“ab” ] hashtable 结构,不能扩容
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
}
将上述代码反编译,生成反编译结果,对于String s4 = s1 + s2;部分的代码如下:
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
它先创建了StringBuilder()对象,然后调用了init()构造方法。aload_1把s1这个参数加载进来,即从局部变量表中拿到s1参数a,与astore1相反。接下来调用了append方法,s1作为参数。aload2又把s2拿到了,即字符串b,作为append方法。最后使用了一个toString方法,最后astore_4,即把toString转换后的结果存入到4号的局部变量中。
我们可以看看StringBuilder的toString原码
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
即创建了一个新的值为ab的对象,存入到s4这个对象中。
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
System.out.println(s3==s4);
结果是false
虽然它们值是相同的,但是s3是在串池中,而s4是new出来的字符串对象,是在堆里面的。它们是两个对象。所以打印false
public static void main(String[] args) {
String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
}
反编译
生成的最后一行代码的字节码如下:
29: ldc #4 // String ab
31: astore 5
我们可以看到,它不是先找a再找b最后拼接到一起,而是直接找到的就是直接拼接好的ab这个符号,并且存入到局部变量里面,下标为5的位置。
而这是String s3 = “ab”;的字节码
6: ldc #4 // String ab
8: astore_3
在开始的时候,串池没有ab这个对象,s3创建出来了,就放入到串池,而s5再次引用ab这个字符串时候,就先会去字符串常量池中找,找到了,然后就会直接用串池中字符串对象。所以存储s3的对象和存储s5的对象,都是串池中的字符串对象。
最后发现下面结果输出为真。
String s3 = "ab";
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5);
结果为true
javac 在编译期间的优化,结果已经在编译期确定为ab
而上面拼接的是变量,既然是变量,说明以后引用的时候可能会发生修改,所以值不能确定。所以在运行期间用stringbuilder的方式来拼接。
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
我们有以下代码
String s=new String("a")+new String("b");
经过前面学习,我们知道了
常量a,b都被放入到了常量池中
new出来的两个对象String(“a”)和String(“b”)被放入到了堆中。他们值虽然相同,但是对象不同。
s又引用了对象,new String(“ab”),它只存在于堆中,不存在串池中。
即常量池中存在[“a”,“b”]
而堆中存在new String(“a”),new String(“b”),new String(“ab”)
我们能不能把ab存入到常量池中呢,可以通过intern方法
intern方法将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回。
public class Main {
public static void main(String[] args) {
//"a" "b" 被放入串池中,str则存在于堆内存之中
String str = new String("a") + new String("b");
//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
String st2 = str.intern();
//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
String str3 = "ab";
//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
System.out.println(str == st2);
System.out.println(str == str3);
}
}
返回结果为true,true
如果将在最开始定义了String x="ab"呢
public class Main {
public static void main(String[] args) {
//此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
String str3 = "ab";
//"a" "b" 被放入串池中,str则存在于堆内存之中
String str = new String("a") + new String("b");
//此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
String str2 = str.intern();
//false
System.out.println(str == str2);
//false
System.out.println(str == str3);
//true
System.out.println(str2 == str3);
}
}
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
常量池中的字符串仅是符号,只有在被用到时才会转化为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder
字符串常量拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
注意:无论是串池还是堆里面的字符串,都是对象
用来放字符串对象且里面的元素不重复
面试结果如下:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
// 问,如果调换了【最后两行代码】的位置呢
System.out.println(x1 == x2); //调换前true,调换后fals
}
此时所有的问题是不是都迎刃而解了
在1.6中是常量池的一部分,随常量池存储在永久代中,1.7开始之后就放入到了堆中。因为永久代的内存回收效率很低,永久代很难触发垃圾回收,需要等到老年代空间不足才会回收,触发效率不高。Springtable是非常常用的,放入到常量池中容易导致永久代内存不足。而堆里面Springtable触发垃圾回收的情况比较简单。
StringTable在内存紧张时,会发生垃圾回收。
SpringTable底层是哈希表,哈希表性能与其大小有关,如果哈希表桶的个数比较多,元素分散,哈希碰撞的几率就比较小,查找效率较高。
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
-XX:StringTableSize=xxxx
考虑是否需要将字符串对象入池
可以通过intern方法减少重复入池,相同的地址intern之后,在内存中只会存储一份,这样就能减少字符串对内存占用。