字符串常量池的设计思想
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能。
- JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
- 为字符串开辟一个字符串常量池,类似于缓存区。
- 创建字符串常量时,首先查询字符串常量池是否存在该字符串。
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
三种字符串操作(Jdk1.7 及以上版本)
- 直接赋值字符串
String name = "lilei"; // name 指向常量池中的引用
这种方式创建的字符串对象,只会在常量池中。
因为有 "lilei" 这个字面量,创建对象 name 的时候,JVM 会先去常量池中通过 equals(key) 方法,判断是否存在相同的对象。
不存在:会在常量池中创建一个新对象,再返回引用。
存在:直接返回该对象在常量池中的引用。
- new String();
1 String name = new String("meimei"); // name 指向内存中的对象引用
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
因为有 "meimei" 这个字面量,所以会先检查字符串常量池中是否存在字符串 "meimei"。
不存在:先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象 "meimei";
存在:直接去堆内存中创建一个字符串对象 "meimei";
最后,将内存中的引用返回。
- intern方法
String s1 = new String("hanxin");
String s2 = s1.intern();
System.out.println(s1 == s2); // false
String 中的 intern 方法是一个 native 的方法,当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(oject) 方法确定),则返回池中的字符串。否则,将intern 返回的引用指向当前字符串 s1 (jdk1.6版本需要将 s1 复制到字符串常量池里)。
字符串常量池位置
Jdk1.6及之前:有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池。
Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里。
Jdk1.8及之后:无永久代,运行时常量池在元空间,字符串常量池里依然在堆里。
字符串常量池设计原理
字符串常量池底层是 hotspot 的 C++ 实现的,底层类似一个HashTable,保存的本质上是字符串对象的引用。 看一道比较常见的面试题,下面的代码创建了多少个 String 对象?
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
// 在 JDK 1.6 下输出是 false,创建了 6 个对象
// 在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
// 当然我们这里没有考虑GC,但这些对象确实存在或存在过
为什么输出会有这些变化呢?主要还是字符串池从永久代中脱离、移入堆区的原因,intern() 方法也相应发生了变化:
1、在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字 符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。
2、在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上面两个图,可以得出:
JDK 1.6 字符串池溢出会抛出 OutOfMemoryError: PermGen space ,而在 JDK 1.7 及以上版本抛出 OutOfMemoryError: Java heap space 。
String常量池问题的几个例子
示例1:
String s1 = "lilei";
String s2 = "li" + "lei";
String s3 = "meimei2";
String s4 = "meimei" + 2;
System.out.println(s1 == s2); // true
System.out.println(s3 == s4); // true
分析:当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以 s2 也同样在编译期就被优化为一个字符串常量 "lilei",所以 s2 也是常量池中 ” lilei” 的一个引用,s3与s4同理。
示例2:
String s0 = "lilei";
String s1 = new String("lilei");
String s2 = "li" + new String("lei");
System.out.println(s0 == s1); // false
System.out.println(s0 == s2); // false
System.out.println(s1 == s2); // false
分析:用new String() 创建的字符串不是常量,不能在编译期就确定,所以 new String() 创建的字符串不放入常量池中,它们有自己的地址空间。 s0 还是常量池 中 "lilei” 的引用,s1 因为无法在编译期确定,所以是运行时创建的新对象 ”lilei” 的引用,s2 因为有后半部分 new String(”lei”) 所以也无法在编译期确定,所以也是一个新创建对象 ”lilei” 的引用。
示例3:
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println(a == b); //result = false
private static String getBB()
{
return "b";
}
分析:JVM 对于字符串引用 bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和 "a" 来动态连接并分配地址为 b,故上面程序的结果为false。
关于String是不可变的
String s = "a" + "b" + "c"; //就等价于String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s1 = a + b + c;
System.out.println(s == s1); //false
s1 这个就不一样了,可以通过观察其JVM指令码发现s1的 "+" 操作会变成如下操作:
0: ldc #2 // String abc
2: astore_1
3: ldc #3 // String a
5: astore_2
6: ldc #4 // String b
8: astore_3
9: ldc #5 // String c
11: astore 4
13: new #6 // class java/lang/StringBuilder
16: dup
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
20: aload_2
21: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_3
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: aload 4
30: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore 5
38: return
最后再看一个例子:
//字符串常量池:"计算机"和"技术" 堆内存:str1引用的对象"计算机技术"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对 象引用
String str2 = new StringBuilder("计算机").append("技术").toString(); //没有出现"计算机技术"字面量,所以不会在常量池里生 成"计算机技术"对象
System.out.println(str2 == str2.intern()); //true
//计算机技术 在池中没有,但是在heap中存在,则intern时,会直接返回该heap中的引用
//字符串常量池:"ja"和"va" 堆内存:str1引用的对象"java"
//堆内存中还有个StringBuilder的对象,但是会被gc回收,StringBuilder的toString方法会new String(),这个String才是真正返回的对 象引用
String str1 = new StringBuilder("ja").append("va").toString(); //没有出现"java"字面量,所以不会在常量池里生成"java"对象
System.out.println(str1 == str1.intern()); //false
//java是关键字,在JVM初始化的相关类里肯定早就放进字符串常量池了
String s1 = new String("test");
System.out.println(s1 == s1.intern()); //false
//"test"作为字面量,放入了池中,而new时s1指向的是heap中新生成的string对象,s1.intern()指向的是"test"字面量之前在池中生成的 字符串对象
String s2 = new StringBuilder("abc").toString();
System.out.println(s2 == s2.intern()); //false
//同上
八种基本类型的包装类和对象池
java 中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是 Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外 Byte,Short,Integer,Long,Character 这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,也即对象不负责创建和管理大于 127 的这些类的对象。因为一般这种比较小的数用到的概率相对较大。