为什么要设计字符串常量池
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
- 为字符串开辟一个字符串常量池,类似于缓存区
- 创建字符串常量时,首先坚持字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符,将其放入池中,并返回该引用。
实现的基础
字符串常量池实现的前提条件就是Java中String对象是不可变的(final),这样可以安全保证多个变量共享同一个对象。如果Java中的String对象可变的话,一个引用操作改变了对象的值,那么其他的变量也会受到影响,显然这样是不合理的。
运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收
堆(Heap)
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Heap是被所有线程共享的一块内存区域,在虚拟机启动时被创建。
Heap的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存。
栈(Stack)
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
每个栈中的数据(原始类型和对象引用)都是私有的
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失
字符串常量池
在jdk1.7之前的版本中,字符串常量池是存在于永久代中,在永久代和Heap中的地址是完全分离的,不会直接饮用
在jdk1.7+之后的版本中,字符串常量池被放在了Heap中。
创建String对象时的常见误区
String s1 = "A" + "hello";//1个对象 默认变成 Ahello 保存到s1 原因Java对String 的+做了优化:直接转换为一个字符串。 只在常量池中创建一个"Ahello"对象
String s2 = "A";//1个对象 A
String s3 = s2 + "hello";//1个对象 hello 原因:在常量池中找"A" "hello" "Ahello",能找到"A"和"Ahello" 所以只新建一个"hello"
String s4 = new String("A") + new String("hello");//2个对象 A和hello 原因:先在堆中创建2个String对象。然后去常量池中找"A" "hello" "Ahello",能找到。不新建了
public class Demo {
public static void main(String[] args) {
String s = "abc";
/*
* 1.在堆内存中创建String类型的对象
* 2.在常量池中查找是否有 "abc"字面量对象
* 3.没有。则在常量池中新建一个"abc"字面量
* 4.把"abc"对象的地址值赋值给String对象中的字段
*/
String s2 = new String("abc");
String s3 = "A"+"B"+"C";// 1个对象 原因Java对String 的+做了优化:直接转换为一个字符串。 只在常量池中创建一个"ABC"对象
String s4 = "AB"+"C";//0个 原因:先在常量池中找“ABC". 能找到,不新建对象
String s5 = new String("ABC");//1个 原因:先在堆中创建一个String对象。然后去常量池中找”ABC",能找到。不新建了
/*
* 7个 原因:new了3次String:3个对象 字面量:AB ABC(常量池已经有了) ABCDE C DE
*/
String s6 = new String("AB")+new String("C")+new String("DE");
}
}
代码1
String str1 = "hello";
创建了一个"hello"对象,引用指向常量池
代码2
String str1 = "hello" + "world";
显然创建了一个"helloworld"对象,引用指向常量池
代码3
String str2 = new String("hello");
创建了两个对象,一个在常量池,一个new在堆,引用str2指向堆中"hello"
代码4
String str1 = new String("A"+"B") ;
创建了常量池中的"A"“B”、堆中new的"AB"共三个对象,引用str1指向堆中的"AB"
举例
代码5
public class StringDemo {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String(str1);
System.out.println(str1 == str2); // 运行结果:false
}
}
相比于代码3,创建了两个对象,常量池中的"hello"、堆中new的"hello"
不懂的
代码6
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?????????????
代码7
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
}
}
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中创建三个对象。