2.2 字符串常量池

325 阅读9分钟

2.2 字符串常量池

​ 我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量是,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串(重点)

​ java中的常量池,实际上分为两种形态:静态常量池和运行时常量池

静态常量池:即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)子面量,还包含类,方法的信息,占用class文件绝大部分空间。

运行时常量池:是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

ex:

String a = "wannabe";
String b = "wannabe";

a,b 和字面上的"wannabe"都是指向JVM字符串常量池中的"wannabe"对象,它们指向同一个对象。

String c = new String("wannabe");

new 关键字一定会产生一个新对象wannabe(与上面的wannabe不同),同时这个对象是存储在堆中。所以上面应该产生两个对象,保存在栈中的c和保存在堆中的wannabe。但是在java中根本就是不存在两个完全一模一样的字符串对象。故堆中的wannabe应该是引用字符串常量池中wannabe,所以c,wannabe,池wannabe关系应该是c——>wannabe——>wannabe。

关系如下:

通过上面的图我们可以非常清晰的认识到它们之间的关系。所以我们修改内存中的值,他变化的是所有。

总结:

虽然a,b,c, wannabe是不同的对象,但是String的内部结构我们是可以理解上面的。String c = new String ("wannabe");虽然c的内容是创建在堆中,但是他的内部value还是指向String常量池的wannabe的value,它构造wannabe时所用的参数依然是wannabe字符串常量。

再来看几个例子:

ex1:

/**
 *	采用字面值的方式赋值
 */
public void ex1(){
    String str1 = "aaa";
    String str2 = "aaa";
    System.out.println(str1 == str2); //true 可以看出str1跟str2是指向同一个对象
}

执行上述代码,结果为:true

分析:当执行String str1 = "aaa"时,

JVM首先会去String常量池中查找是否存在"aaa"这个对象,

如果不存在,则在字符串中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str1,这样str1会指向池中"aaa"这个字符串对象;

如果存在,则不会创建任何对象,直接将池中"aaa"这个对象的地址返回,赋值给字符串常量;

当创建字符串对象str2时,String常量池中已经存在"aaa"这个对象,直接把对象"aaa"的引用地址返回给str2,这样str2指向了池中"aaa"这个对象,也就是说str1和str2指向了同一个对象。

因此System.out.println(str1 == str2); 输出:true

ex2:

/**
 * 采用new关键字新建一个字符串对象
 */
/**
 *	采用字面值的方式赋值
 */
public void ex2(){
    String str3 = new String("aaa");
    String str4 = new String("aaa");
    System.out.println(str3== str4); //false 可以看出用new的方式生成不同的对象
}

执行上述代码,结果为:false

分析:采用new关键字新建一个字符串对象时,

JVM首先会去String常量池中查找是否存在"aaa"这个对象,

如果有,则不在池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str3,这样,str3就指向了堆中创建的这个"aaa"字符串对象;

如果没有,则会首先在字符串池中创建一个”aaa“字符串对象,然后再堆中创建一个”aaa“z字符串对象,再将堆中这个”aaa“字符串对象的地址返回给str3引用,这样str3指向了堆中创建的这个”aaa“字符串对象

当执行String str4 = new String("aaa")时,因为采用new关键字创建对象时,每次new出来的都是一个新的对象,也即时说引用str3和str4指向的时两个不同的对象。

因此 System.out.println(str3== str4); 输出:false

ex3:

/**
 *	编译期确定
 */
public void ex3(){
    String s0 = "helloworld";
    String s1= "helloworld";
    String s2= "hello"+"world";
    System.out.println(s0 == s1); //true 可以看出s0跟s1是指向同一个对象
    System.out.println(s0 == s2); //true 可以看出s0跟s2是指向同一个对象
}

执行上述代码,结果为:true,true

分析:因为例子中的s0和s1中的"helloworld"都是字符串常量池,它们编译期就被确定了,所以s0 = = s1 为true;而"hello"和"world"也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样再编译期就会被解析为一个字符串常量,所以s2也是常量池中"helloworld"的一个引用。所以我们得出s0==s1==s2;

ex4:

/**
 *	一、编译期无法确定
 */
public void ex4(){
    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 
}

执行上述代码,结果为:false,false,false

分析用new String()创建的字符串不是常量,不能再编译期就确定,所以new String()创建的字符串不放入常量池中,它们有自己的地址空间;

s0还是常量池中的”helloworld“的引用,s1因为无法再编译期确定,所以是运行时创建的新对象"helloworld"的引用,s2因为有后半部分new String("world")所以也无法再编译期确定,所以也是一个新创建对象“helloworld”的引用。

ex5:

/**
 *	二、编译期无法确定
 */
public void ex5(){
    String str1 = "abc";
    String str2 = "def";
    String str3 = str1 + str2;
    System.out.println(str3 == "abcdef"); //false 
}

执行上述代码,结果为:false

分析:因为str3指向堆中的“abcdef”对象,而"abcdef"是字符串池中的对象,所以结果是false。

**JVM对String str1 ="abc" 对象放在常量池中是在编译时做的,而String str3 = str1+str2是在运行时刻才知道的。new对象也是在运行才做的。**而这段代码总共创建了5个对象,字符串池中两个,堆中三个。+运算符会在堆中建立两个String对象,这两个对象的值分是“abc”和“def”,也就是说从字符串池中复制这两个值,然后再堆中创建两个对象,然后再建立对象str3,然后将"abcdef"的堆地址赋值给str3;

步骤:

  1. 栈中开辟一块中间存放引用str1,str1指向池中String常量”abc“;
  2. 栈中开辟一块中间存放引用str2,str2指向池中String常量”def“;
  3. 栈中开辟一块中间存放引用str3;
  4. str1 + str2 通过StringBuilder的最后一步toString()方法还原一个新的String对象”abcdef“,因此堆中开辟一块空间存放此对象;
  5. 引用str3指向堆中(str1+str2)所还原的新String对象;
  6. str3指向的对象再堆中,而常量”abcdef“在池中,输出为false;

ex6:

/**
 *	编译期优化
 */
public void ex6(){
    String s0 = "a1";
    String s1 = "a"+1;
    System.out.println(so == s1);	//true
    
    String s2 = "atrue";
    String s3 = "a"+"true";
    System.out.println(s2 == s3);	//true
    
    String s4 = "a3.4";
    String s5 = "a"+3.4;
    System.out.println(s4 == s5);	//true
}

执行上述代码,结果为:true,true,true

分析:在程序编译期,JVM就将常量字符串的”+“连接优化为连接后的值,拿”a“+1来说,经编译期优化后在class中就已经是a1。在编译期期字符串常量的值就确定下来,故上面程序最终的结果都为true

ex7:

/**
 *	三、编译期无法确定
 */
public void ex7(){
    String s0 = "ab";
    String s1 = "b";
    String s2 = "a" + s1;
    System.out.println(s0 == s2); //false 
}

执行上述代码,结果为:false

分析:JVM对于字符串引用,**由于在字符串的”+“连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即”a“+s1无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给s2。**所以上面程序的结果也就为false;

ex8:

/**
 *	比较字符串常量的”+“和字符串引用的”+“的区别
 */
public void ex8(){
    String test = "javalanguagespecification";
    String str = "java";
    String str1 = "language";
   	String str2 = "specification";
    System.out.println(test =="java"+"language"+"specification"); //true
    System.out.println(test ==str+str1+str2); //false
}

执行上述代码,结果为:true,false

分析: 为什么出现上面的结果呢?这是因为,字符串字面量拼接操作是在java编译器编译期就执行了,也就是说编译器编译时,直接把”java“,”language“和”specification“这三个字面量进行”+“操作得到一个”javalanguagespecification“常量,并且直接将这个常量放入字符串池中,这样做实际上是一种优化,将3个字面量合成一个,避免了创建多余的字符串对象,而字符串引用的”+“运算时再java运行期间执行的,即str+str2+str3在程序执行期间才会进行计算,它会在堆内存中重新创建一个拼接后的字符对象。

总结来说就是:字面量”+“拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的”+“拼接运算是在运行时进行的,新创建的字符串存放在堆中。

对于直接相加的字符串,效率很高,因为在编译器便确定了它的值,也就是说”hello“+”world“;的字符串相加,在编译期间便被优化成了”helloworld“。

对于间接相加(即包含字符串引用),形如s1+s2+s3;效率要比直接相加低,因为编译器不会对引用变量进行优化。

ex9:

/**
 *	编译期确定
 */
public void ex9(){
    String s0 = "ab";
    final String s1 = "b";
    String s2 = "a" + s1;
    System.out.println(s0 == s2); //true
}

执行上述代码,结果为:true

分析: 和ex7中唯一不同的是s1字符串加了final修饰,对于final修饰的变量,它在编译是被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的”a"+s1和“a"+"b"效果一样的。故上面程序的结果为true;

ex10:

/**
 *	编译期无法确定
 */
public void ex10(){
    String s0 = "ab";
    final String s1 = getS1();
    String s2 = "a" + s1;
    System.out.println(s0 == s2); //false
}
private static String getS1(){
    return "b";
}

执行上述代码,结果为:false

分析:这里虽然将s1用final修饰了,但是由于期赋值是通过方法调用返回,那么它的值只能在运行期间确定,因此s0和s2指向的不是同一个对象,故上面程序的结果false。

github:github.com/ccy524946/t…