java中的String和String常量池
在描述理论之前,我们先看段代码:
1 public static void main(String args[]) {
2
3 String s1 = "test";
4 String s2 = "test";
5 String s3 = "te" + "st";
6 String s4 = new String("test");
7 String s5 = s4.intern();
8 String s6 = "te";
9 String s7 = "st";
10 String s8 = s6 + s7;
11
12 System.out.println(s1 == s2); // true
13 System.out.println(s1 == s3); // true
14 System.out.println(s1 == s4); // false
15 System.out.println(s1 == s5); // true
16 System.out.println(s1 == s8); // false
17 System.out.println(s1.equals(s8)); // true
18 }
各位读者可以先按照自己的理解想一下代码中等式的结果,看看最后的结果是不是一样。如果一样,那么恭喜您,您对JVM中运行时数据区,尤其是方法区里面的运行时常量池的理解已经相当透彻,就可以忽略这篇文章啦;如果有点出入,那我们就值得一起来看看这是怎么回事儿。
如果想要知道上面的代码结果的成因,我们起码需要先知道一些名词:Java堆、方法区、运行时常量池
Java堆
JVM运行时数据区的一部分。用于存放对象实例,几乎所有的对象都在堆上分配内存。
方法区
JVM运行时数据区的一部分。方法区用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池
方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
其实看完了这三个名词,我们还是不很了解和程序中的结果有什么关系。别急,我们这就开始慢慢表~
第12行结果:
s1和s2的比对结果是true,原因就是上面运行时常量池中说到的其作用:
用于存放编译器生成的各种字面量
用于存放编译器生成的各种字面量
用于存放编译器生成的各种字面量
重要的事情说三遍,因为本文中大部分的解释都和这句话有关。s1、s2在赋值时,均使用的字符串字面量。这种字面量会直接放入class文件的常量池中,从而实现复用。编译载入运行时常量池后,s1、s2指向的是同一个内存地址,所以结果为true。
从这里我们也能得到一个重要信息,运行时常量池存在的原因之一就是节省内存。
第13行结果:
s1和s3的比对结果是true。这里不得不说下编译的事儿:s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "te" + "st" 在class文件中被优化成String s3 = "test"。加载到运行时常量池后和s1、s2还是只想同一个内存地址。
第14行结果:
这里面就牵扯到java堆的概念了。s4使用new来显示的创建对象实例是在堆中分配的,而s1是在方法区的常量池中,地址一定不同,所以结果为false
第15行结果:
String.intern()这个方法会尝试将test字符串添加到常量池中,并返回其在常量池中的地址;因为常量池中已经有了test字符串(s1、s2、s3都指向),所以intern方法直接返回地址。地址相同,结果为true
第16行结果:
s8是由s6和s7拼接而成的,但是对于这种情况,编译器在编译的时候并不能像第5行那样直接帮忙拼好再存入class静态常量池,因为编译器还没有智能到认为“s6和s7就是字面量,所以s8也是字面量”这种程度(虽然我认为也不是很难的事儿),所以在编译后载入运行时常量池的时候也就不可能使用相同的地址。所以结果为false
第17行结果:
上面所有的比对都是比对引用地址,而这句是比对内容。而s1和s8的内容都是“test”,所以结果相等。