Java的常量池有字符串常量池、Class常量池和运行时常量池等,一想到这么多的常量池头都大了,今天就来梳理一下这些常量池的区别。
一、运行时常量池
运行时常量池是方法区的一部分,即JDK1.8中的元空间。JVM将类加载到内存中后,会将Class常量池中的内容存放到运行时常量池中。
二、Class文件常量池
Clas文件中除了有类的版本、字段等描述信息外,还有常量池表(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后会存放在方法区中的运行时常量池。常量池主要存放两大类常量:字面量和符号引用。
1. 字面量
字面量有文本字符串、被声明为final的常量值等
2. 符号引用
符号引用有:
- 类和接口的全限定名(全限定名:包名+类名,如java.lang + String)
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
三、字符串常量池
为了减少字符串对象的重复创建,复用字符串对象,提高性能和减少内存开销,在堆中开辟了一段内存用于存放字符串常量。JDK1.6及之前版本,字符串常量池存放在方法区;JDK1.7及以后版本,字符串常量池移到堆中。
1. intern方法
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
intern方法是一个本地方法,如果字符串常量池中存在当前字符串,则直接返回当前字符串;如果常量池中没有当前字符串,则把字符串放入常量池中并返回它的引用。
jdk1.6和jdk1.7中由于字符串常量池的由永久代移到了堆中,导致调用intern方法在不同jdk版本会出现意想不到的结果。先看下面一段代码:
private static void testString() {
String s1 = new String("1"); // (1)
s1.intern();
String s2 = "1"; // (2)
System.out.println(s1 == s2);
String s3 = new String("1") + new String("1"); // (3)
s3.intern();
String s4 = "11"; // (4)
System.out.println(s3 == s4);
}
jdk 1.6 输出结果:
false
false
jdk 1.7 输出结果:
false
true
jdk6结果分析:
jdk6中字符串常量池在永久代中,使用语句(1)双引号赋值持有对永久代的一个引用;使用语句(2)对字符串赋值时,new 出来的对象是在JVM heap区域,s2持有的是对堆区的引用;又因为堆和永久代是分隔开的,所以两个引用的地址必然不相等,即使调用了intern方法也不会改变结果,因此返回false。
jdk7结果分析:
jdk7中字符串常量池存放在堆中,语句(1)生成了字符串常量池中的“1” 和JVM堆中的字符串“1”这两个对象,s1.intern() 是去字符串常量池查看是否存在字符“1”,并不会改变s1指向堆这个事实;语句(2)生成一个指向字符串常量池的引用s2,因此 s1==s2 返回false。
语句(3)最终生成了字符串常量池中的 “1” 和JVM堆中的字符串 “11” 这两个对象,s3指向堆中字符串的一个引用,由于编译器并不会对这种由语句(1)生成的字符串进行优化,因此字符串常量池此时并没有“11”字符串。此时调用s3.intern方法,如果堆中有对应的字符串对象,则字符串常量池直接存储堆中的引用而不是在堆中的常量池再存储一份字符串对象。因此s3和s4都指向同一个对象,因此返回true。
重要点:
- Jdk7 String的intern方法,如果存在堆中的对象,会直接保存对象的引用,不会重新创建对象。
四、Java基本数据类型的包装类常量池
Java中基本数据类型的包装类中实现了常量池技术的有六种Byte、Short、Integer、Long、Character、Boolean,剩下的Float和Double没有实现常量池技术。下面以代码对整型的常量池进行说明,其它类型不再赘述。Integer类会缓存[-128,127]范围内的值。
public static void main(String[] args) {
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
Integer i7 = 128;
Integer i8 = 128;
// true,因为缓存i1和i2指向了同一个对象
System.out.println("i1=i2: " + (i1 == i2));
// true, 等式右边有算术运算会做自动拆箱操作,比较的是数值相等与否
System.out.println("i1=i2+i3: " + (i1 == i2 + i3));
// false, i1指向的是缓存,i4指向的新创建的Integer对象
System.out.println("i1=i4: " + (i1 == i4));
// false,i4、i5指向的新创建的不同Integer对象
System.out.println("i4=i5: " + (i4 == i5));
// true,会做自动拆箱运算,比较的是数值
System.out.println("i4=i5+i6: " + (i4 == i5 + i6));
// true,会做自动拆箱运算,比较的是数值
System.out.println("40=i5+i6: " + (40 == i5 + i6));
// false,超过了缓存大小,因此指向的是不同的对象
System.out.println("i7=i8: " + (i7 == i8));
}