为什么要设计字符串常量池
-
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
-
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 创建字符串常量时,首先坚持字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符,将其放入池中,并返回该引用。
实现的基础
字符串常量池实现的前提条件就是Java中String对象是不可变的(final),这样可以安全保证多个变量共享同一个对象。如果Java中的String对象可变的话,一个引用操作改变了对象的值,那么其他的变量也会受到影响,显然这样是不合理的。
运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收
堆(Heap)
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Heap是被所有线程共享的一块内存区域,在虚拟机启动时被创建。
Heap的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存。
栈(Stack)
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
每个栈中的数据(原始类型和对象引用)都是私有的
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失
字符串常量池
在jdk1.7之前的版本中,字符串常量池是存在于永久代中,在永久代和Heap中的地址是完全分离的,不会直接饮用
在jdk1.7+之后的版本中,字符串常量池被放在了Heap中。
创建String对象时的常见误区
为常量赋值
代码
String str1 = "hello";
这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"hello"的这么一个对象,它的判断依据是String 类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
代码
String str1 = "hello" + "world";
这行代码被执行的时候只创建了一个对象,在编译期对其进行优化,将其解析为一个"helloworld"字符串常量,所以str1 也是常量池中"helloworld”的一个引用。
创建对象的赋值
代码
String str2 = new String("hello");
在执行过程中,JVM首先在字符串常量池中查看字符串对象“hello”是否存在,若不存在,则现在字符串常量池中创建“hello”对象,然后再Heap中创建一个新的“hello”字符串对象,即创建两个对象。若字符串常量池中已经存在了字符串对象“hello”,则直接在堆中创建一个字符串对象“hello”,不许在字符串常量池中创建对象,即只创建了一个对象。但是不管是那种方式,栈中的str2始终指向的是Heap中的字符串对象。
代码
String str1 = new String("A"+"B") ;
在执行过程中,,JVM先查看字符串常量池中是否有字符串对象"A"和"B",若不存在,则在字符串常量池中创建"A"和"B"对象,然后再Heap中创建一个新的对象,由str1指向Heap中的对象,即创建三个对象。若字符串常量池中已经存在了字符串对象"A"和"B",则直接在堆中创建一个对象,即只创建了一个对象。
举例
代码
public class StringDemo {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String(str1);
System.out.println(str1 == str2); // 运行结果:false
}
}
这里一共创建了两个对象,str1使用常量赋值的方式,str1直接指向了字符串常量池中的对象,而str2使用new()来创建对象,str2会指向堆中的对象,即为false。
代码
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
System.out.println( s0==s1 ); //false
System.out.println( s0==s2 ); //false
System.out.println( s1==s2 ); //false
用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。
s0还是常量池中"helloworld”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"helloworld”的引用,s2因为有后半部分new String(”world”)所以也无法在编译期确定,所以也是一个新创建对象"helloworld”的引用。
代码
String str1="abc";
String str2="def";
String str3=str1+str2;
System.out.println(str3=="abcdef"); //false
因为str3指向堆中的"abcdef"对象,而"abcdef"是字符串池中的对象,所以结果为false。JVM对String str=“abc"对象放在常量池中是在编译时做的,而String str3=str1+str2是在运行时刻才能知道的。new对象也是在运行时才做的。而这段代码总共创建了5个对象,字符串池中两个、堆中三个。+运算符会在堆中建立来两个String对象,这两个对象的值分别是"abc"和"def”,也就是说从字符串池中复制这两个值,然后在堆中创建两个对象,然后再建立对象str3,然后将"abcdef"的堆地址赋给str3。
代码
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
JVM对于字符串引用,由于在字符串的"+“连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a” + s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。所以上面程序的结果也就为false。
代码
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = true
这里s1字符串加了final修饰,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + s1和"a" + "b"效果是一样的。故上面程序的结果为true。
代码
public void test(){
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); //result = false
}
private static String getS1() {
return "b";
}
这里虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
代码
public class StringDemo {
public static void main(String[] args) {
String str1 = "ab";
String str2 = new String("ab");
String str3 = "a" + "b";
String str4 = new String("a") + new String("b");
System.out.println(str1 == str3); // true
System.out.println(str1 == str4); // false
System.out.println(str2 == str3); // false
System.out.println(str2 == str4); // false
}
}
这里str1、str2、str3和上述情况一致。主要解释一下str4
String str4 = new String(“a”) + new String(“b”)可分解为new String(“a”)和new String(“b”),在执行这两步操作时,都会先去字符串常量池中查找是否存在“a”和“b”,若没有,则在字符串常量池中创建“a”和“b”两个对象,然后再在Heap中创建new String(“a”)和new String(“b”)两个对象。否则直接在Heap中直接创建new String(“a”)和new String(“b”)两个对象。之后会将两个字符串进行拼接,拼接后会再在Heap中创建一个new String(“a”+“b”)对象。
因此若原本字符串常量池中没有“a”“b”,执行String str4 = new String(“a”) + new String(“b”)会创建五个对象,若字符串常量池中已经存在了"a"和"b"则只在Heap中创建三个对象。
intern()方法的使用
intern函数的作用是将对应的符号常量进入特殊处理,在1.6以前 和 1.7以后有不同的处理:
在1.6中,intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量。
在1.7中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中。
代码
public class StringDemo {
public static void main(String[] args) {
String str1 = "ab";
String str2 = str1.intern();
System.out.println(str1 == str2); // true
}
}
str1是最普通的常量赋值,会直接在字符串常量池中创建出"ab"对象
str1调用intern方法得到返回值赋值给str2,intern方法会现在字符串常量池中检测是否已经存在"ab"字符串对象,若已经存在,直接把字符串常量池里"ab"对象的地址赋值给str2,所以str1==str2为true。
代码
String str1 = new String("ab");
System.out.println(str1.intern()==str1);//false
System.out.println(str1.intern()=="ab");//true
执行intern方法时,如果常量池中存在和String对象相同的字符串,则返回常量池中对应字符串的引用,如果常量池中不存在对应的字符串,则添加该字符串到常量中,并返回字符串引用。
代码
String str1 = new String("a") + new String("b");
String str2 = str1.intern();
String str3 = "ab";
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // true
这里str1的创建在字符串常量池中创建对象"a"和"b",在堆中创建对象new String(“a”)、new String(“b”)和new String(“a”+“b”),一共五个对象
调用str1的intern方法,首先在字符串常量池中查找"ab"是否存在,若不存在,则将堆中的new String(“a+b”)对象的引用复制到字符串常量池中,则str1 == str2为true
str3直接使用常量赋值,JVM虚拟机先去字符串常量池中查询对象"ab"是否存在,这时,将字符串池中的new String(“a”+“b”)对象的引用传递给str3,则str1==str3为true。
代码
String str1 = new String("ja") + new String("va");
System.out.println(str1.intern() == str1); //false
这里因为java在启动的时候,会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添加到字符串常量池中的”java“的引用,所以跟str1的原地址不一样。
intern总结
当字符串对象调用intern方法时,JVM虚拟机会先去字符串常量池中查找该对象是否在字符串常量池中存在,若存在,则直接返回字符串常量池该对象的引用。
若字符串常量池不存在该对象,则直接把堆中的该对象的引用复制到字符串常量池中,然后返回字符串常量池中该引用的值(即堆中该对象的引用)。