字符串常量池
字符串常量池的设计思想
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
- JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 创建字符串常量时,首先查询字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
三种字符串操作
1、直接赋值字符串
String s = "jjs"; // s指向常量池中的引用
这种方式创建的字符串对象,只会在常量池中。
因为有"jjs"这个字面量,创建对象s的时候,JVM会先去常量池中通过 equals(key) 方法,判断是否有相同的对象
如果有,则直接返回该对象在常量池中的引用;
如果没有,则会在常量池中创建一个新对象,再返回引用。
2、new String()
String s1 = new String("jjs"); // s1指向内存中的对象引用
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
步骤大致如下:
因为有"jjs"这个字面量,所以会先检查字符串常量池中是否存在字符串"jjs"
不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"jjs";
存在的话,就直接去堆内存中创建一个字符串对象"jjs";
最后,将内存中的引用返回。
3、intern方法
String s1 = "jjs";
String s2 = s1.intern();
1.6及以前版本:
intern会判断字符串常量池中是否含有该字符串字面量,如果有则返回字符串常量池中字符串的引用,如果没有则将字符串复制一份到字符串常量池中,并返回字符串常量池中的引用
1.7以及以后版本:
intern会判断字符串常量池中是否含有该字符串字面量,如果有则返回字符串常量池中字符串的引用,如果灭有则将该字符串放到字符串常量池中,并返回该引用
对象池
java中基本类型的包装类的大部分都实现了常量池技术(严格来说应该叫对象池,在堆上),这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。
举一个例子Integer的valueof方法,当i大于-128并且小于127的时候,就会从对象池中拿到对象
public class Test {
public static void main(String[] args) {
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可以使用对象池
Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
Integer i2 = 127;
System.out.println(i1 == i2);//输出true
//值大于127时,不会从对象池中取对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//输出false
//用new关键词新生成对象不会使用对象池
Integer i5 = new Integer(127);
Integer i6 = new Integer(127);
System.out.println(i5 == i6);//输出false
//Boolean类也实现了对象池技术
Boolean bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);//输出true
//浮点类型的包装类没有实现对象池技术
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2);//输出false
}
}
字符串常见题目
题目一:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
答案:false
首先贴出反编译后的常量池:
Constant pool:
#1 = Methodref #12.#40 // java/lang/Object."<init>":()V
#2 = String #41 // a
#3 = String #42 // b
#4 = String #43 // ab
#5 = Class #44 // java/lang/StringBuilder
#6 = Methodref #5.#40 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#45 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#46 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #47.#48 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #49.#50 // java/io/PrintStream.println:(Z)V
#11 = Class #51 // DeadLockDemo
#12 = Class #52 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 LDeadLockDemo;
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 s1
#25 = Utf8 Ljava/lang/String;
#26 = Utf8 s2
#27 = Utf8 s3
#28 = Utf8 s4
#29 = Utf8 StackMapTable
#30 = Class #23 // "[Ljava/lang/String;"
#31 = Class #53 // java/lang/String
#32 = Class #54 // java/io/PrintStream
#33 = Utf8 Exceptions
#34 = Class #55 // java/util/concurrent/ExecutionException
#35 = Class #56 // java/lang/InterruptedException
#36 = Utf8 SourceFile
#37 = Utf8 DeadLockDemo.java
#38 = Utf8 RuntimeVisibleAnnotations
#39 = Utf8 Ljdk/nashorn/internal/runtime/logging/Logger;
#40 = NameAndType #13:#14 // "<init>":()V
#41 = Utf8 a
#42 = Utf8 b
#43 = Utf8 ab
#44 = Utf8 java/lang/StringBuilder
#45 = NameAndType #57:#58 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#46 = NameAndType #59:#60 // toString:()Ljava/lang/String;
#47 = Class #61 // java/lang/System
#48 = NameAndType #62:#63 // out:Ljava/io/PrintStream;
#49 = Class #54 // java/io/PrintStream
#50 = NameAndType #64:#65 // println:(Z)V
#51 = Utf8 DeadLockDemo
#52 = Utf8 java/lang/Object
#53 = Utf8 java/lang/String
#54 = Utf8 java/io/PrintStream
#55 = Utf8 java/util/concurrent/ExecutionException
#56 = Utf8 java/lang/InterruptedException
#57 = Utf8 append
#58 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#59 = Utf8 toString
#60 = Utf8 ()Ljava/lang/String;
#61 = Utf8 java/lang/System
#62 = Utf8 out
#63 = Utf8 Ljava/io/PrintStream;
#64 = Utf8 println
#65 = Utf8 (Z)V
看看具体的汇编代码:
String s1 = "a";
// 0: ldc #2 // String a
// 2: astore_1
String s2 ="b" ;
// 3: ldc #3 // String b
// 5: astore_2
String s3 = "ab";
// 6: ldc #4 // String ab
// 8: astore_3
String s4 = s1+s2;
// 9: new #5 // class java/lang/StringBuilder
// 12: dup
// 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()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
System.out.println(s3==s4);
从上图来看,当类在编译的时候,会创建出一个常量池,然后执行jvm指令的时候就会根据常量池去查找所需要的东西
当执行下边着几行代码时,都是通过常量池找到这几个常量,然后放入将这几个符号变成字符串对象,最后将字符串对象放入StringTable(串池)中
但是在执行下面这行代码的时候是完全不同的
如下图所示
- 先执行的是一个new,创建了一个StringBuilder对象
- 然后invokespecial 调用了StringBuilder的空参构造方法
- 接着aload_1 读取本地变量1的值,也就是a的值
- 接着invokevirtual调用了append方法,将a的值作为参数
- 继续aload_2读取本地变量2的值,也就是b的值
- 然后invokevirtual调用了append方法,将b的值作为参数
- 又继续invokevirtual调用了toString的方法,走进源码看Stringbuilder中的toString方法
又创建了一个String对象,
然后执行astore 将结果放到本地变量4号位值中
至此就完成了
这几行代码的执行
在最后比较的时候判断s3==s4,由此看来是错误的。s3指向的是StringTable中的字符串对象,而s4指向的是堆中的对象,两个的地址是不相同的,所以false
题目二:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 ="a"+"b";
System.out.println(s3 == s4);
答案:ture
看看具体的汇编代码:
String s1 = "a";
// 0: ldc #2 // String a
// 2: astore_1
String s2 ="b" ;
// 3: ldc #3 // String b
// 5: astore_2
String s3 = "ab";
// 6: ldc #4 // String ab
// 8: astore_3
String s4 = "a"+"b";
// 9: ldc #4 // String ab
// 11: astore 4
不难看出,在执行String s4=“a"+"b";的时候,会先直接找常量池的#4,与执行String s3=”ab“的过程是一样的,因为javac在编译期间就做了优化,”a“+”b“的结果已经是”ab“了
所以结果是ture,都是指向StringTable中的”ab“
题目三:
下面的代码创建了多少个 String 对象?
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);
答案 :
(下面的答案均不算字符串拼接会产生一个StringBuild对象)
1、在 JDK 1.6 下输出是 false,创建了 6 个对象
在 JDK 1.6 中,调用 intern() 首先会在字符串池中寻找 equal() 相等的字符串,假如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代上创建一个实例,将 StringTable 的一个表项指向这个新创建的实例。
(字符串常量池中 he llo hello,堆中的 he llo hello )
2、在 JDK 1.7 及以上的版本输出是 true,创建了 5 个对象
(字符串常量池中 he llo ,堆中的 he llo hello )
在 JDK 1.7 (及以上版本)中,由于字符串池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和 JDK 1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
题目四:
String a = "a1";
String b = "a" + 1;
System.out.println(a == b); // true
String a = "atrue";
String b = "a" + "true";
System.out.println(a == b); // true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println(a == b); // true
分析:JVM对于字符串常量的"+"号连接,将在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。在编译期其字符串常量的值就确定下来,故上面程序最终的结果都为true。
题目五:
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // false
分析:JVM对于字符串引用,由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b。所以上面程序的结果也就为false。
题目六:
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // true
分析:和示例5中唯一不同的是bb字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + bb和"a" + "b"效果是一样的。故上面程序的结果为true。
题目七:
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println(a == b); // false
private static String getBB()
{
return "b";
}
分析:JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和"a"来动态连接并分配地址为b,故上面 程序的结果为false。
题目八:
//字符串常量池:"计算机"和"技术" 堆内存: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
//同上