一、class常量池、字符串常量池、运行时常量池
class常量池:我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);每个class 文件都有一个class常量池;
注:字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等; 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符
字符串常量池:在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
运行时常量池:运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用;JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
二、字符串进入到常量池的原则及两种方法
原则:
方法:
1. new String()的实例调用intern()方法。
执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串或者字符串引用的copy,然后返回该字符串的引用。
2. “”(引号)引起来的内容(字面量)。
引号引起来的字符串,首先从常量池中查找是否存在此字符串,如果不存在则在常量池中添加此字符串对象,然后引用此字符串对象。如果存在,则直接引用此字符串。
注意:new String("ab") 和 new String("a") + new String("b") 是有区别的。执行new String("ab") 这个方法时,首先检查字符串常量池中是否存在“ab”字符串,这时常量池中是存在的“ab”的,因为class文件编译时,jvm已经检测到了“ab”这个字符串常量,就会将其放入字符串常量池中。然而执行new String("a") + new String("b")时,编译器会将其编译成 StringBuilder.append(),在堆区产生一个StringBuilder对象用来存储“ab”,并不会检查字符串常量池,除非显示的调用intern()方法。
三、代码验证
public static void main(String[] args) {
String str = new String("a") + new String("b");
String str1 = "ab";
System.out.println(str == str1);
String str2 = str.intern();
System.out.println(str1 == str2);
}
运行结果:
jdk1.6:false、true
jdk1.7/1.8:false、true
public static void main(String[] args) {
String str = new String("a") + new String("b");
String str2 = str.intern();
String str1 = "ab";
System.out.println(str == str1);
System.out.println(str1 == str2);
}
运行结果:
jdk1.6:false、true
jdk1.7/1.8:true、true
疑问:对于jdk1.6版本上面运行结果是一样的,但是针对于1.7及以上版本版本只是移动了一行代码: String str2 = str.intern();为什么运行结果就不一样了呢?
原因:在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。 new String("a") + new String("b") 编译完后实际上是在堆区创建了一个stringbuilder对象,并未在常量池中放入拘留字符串的引用,str.intern()方法执行完毕以后,JVM首先会检查字符串常量池中是否存在“ab”字符串及“ab”字符串的引用,如果存在则返回常量池中字符串的地址或者引用,如果不存在则拷贝一份“ab”字符串在堆区的引用(即str)放入常量池中。
public static void main(String[] args) {
String str = new String("ab");
String str2 = str.intern();
String str1 = "ab";
System.out.println(str == str1);
System.out.println(str1 == str2);
}
运行结果:
jdk1.6 :false、true
jdk1.7/1.8:false、true
从这个运行结果可以看出 str != str2 ,说明在执行 str = new String("ab")这个语句时,常量池中已经存在了“ab”这个字符串,否则 str2 == str == str1。这个验证了创建字符串常量时,首先检查字符串常量池是否存在该字符串,存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
public static void main(String[] args) {
String str = new String("ab");
String str2 = str.intern();
String str1 = "ab";
String str3 = new String("a") + new String("b");
String str4 = str3.intern();
System.out.println(str == str1);
System.out.println(str1 == str2);
System.out.println(str3 == str4);
System.out.println(str4 == str2);
}
运行结果:
jdk1.6:false、true、false、true
jdk1.7/1.8:false、true、false、true
从这个运行结果可以看出 str != str2 ,说明在执行 new String("a") + new String("b")这个语句时,并未检查检查字符串“ab”在常量池是否存,也并未将字符串并放入池中,调用 str3.intern() 检查字符串“ab”在常量池中已经存在,并返回了该字符串的引用str.intern() 。实际上“ab”这个字符串常量是在编译期就已经放入了字符串常量池中。