一、概述

常量池:编译期被确定,*.class文件中的一部分,包含字面量(Literal)和符号引用(Symbolic Reference)。

字面量:文本字符串、声明为final的常量值(int/long/double...)等。

符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

运行时常量池:方法区的一部分,jvm在完成类装载操作后,将class文件中的常量池载入内存并保存在方法区中。

JDK1.6之前字符串常量池位于方法区。

JDK1.7字符串常量池已经被移至堆。

JDK1.8字符串常量池位移至元空间。

二、字符串常量池

字符串常量池属于运行时常量池的一部分

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // true
System.out.println(s1 == s4); // false
System.out.println(s1 == s9); // false
System.out.println(s4 == s5); // false
System.out.println(s1 == s6); // true
java中==比较的是内存地址

s1 == s2 (true),s1、s2赋值时均使用的字符串字面量"Hello",在编译期间,这种字面量会直接放入class文件的常量池中,载入运行时常量池后,s1、s2指向的是同一个内存地址。

s1 == s3 (true),s3虽然是动态拼接出来的字符串,但所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello";,所以s1 == s3成立。

s1 == s4 (false),s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,编译器不会优化,必须等到运行时才可以确定结果,所以s4指向堆中某个地址。

s4.jpg

s1 == s9 (false),s9是s7、s8两个变量拼接,都是不可预料的,编译器不作优化,运行时拼接成新字符串存于堆中某个地址。

s9.png

s4 == s5 (false),二者都在堆中,但地址不同。

s1 == s6 (true),这两个相等完全归功于intern()方法,s5在堆中,内容为"Hello" ,intern方法会尝试将"Hello"字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了"Hello"字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。

以上所讲仅涉及字符串常量池,实际上还有整型常量池、浮点型常量池等等,但都大同小异

数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,只有这个范围的数字可以用到常量池。

三个非常重要的结论:

必须要关注编译期的行为,才能更好的理解常量池。

运行时常量池中的常量,基本来源于各个class文件中的常量池。

程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。

三、常量池溢出

/**
* jdk1.6 -XX:MaxPermSize=5M OutOfMemoryError: PermGen space
* jdk1.7 -Xmx5M OutOfMemoryError: Java heap space
* jdk1.8 -Xmx5M OutOfMemoryError: GC overhead limit exceeded
* jdk1.8 -XX:MaxMetaspaceSize=2M OutOfMemoryError: Metaspace
*/
public class ConstantPoolOOM {
public static void main(String[] args) throws InterruptedException {
List list = new ArrayList();
int index = 0;
while (true) {
list.add(String.valueOf(index++).intern());
}
}
}

jdk1.6运行时常量池在方法区中,设置-XX:MaxPermSize=5M,导致OutOfMemoryError: PermGen space

jdk1.7运行时常量池在堆中,仅仅保存常量的引用,常量对象在堆中,设置-Xmx5M,导致OutOfMemoryError: Java heap space

jdk1.8运行时常量池在元空间中,仅仅保存常量的引用,常量对象在堆中,

设置-XX:MaxMetaspaceSize=2M,导致OutOfMemoryError: Metaspace

设置-Xmx5M,导致OutOfMemoryError: GC overhead limit exceeded

四、jdk1.6和1.7+常量池的差异

/**
* jdk1.6 false false
* jdk1.7+ true false
*/
public class ConstantPoolTest {
public static void main(String[] args) {
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
}

上面代码在jdk1.6中执行会打印两个false

1.6中intern()方法会把首次出现的字符串实例复制到运行时常量池(方法区)中,并返回方法区中这个实例的地址,而str1 str2的地址都在堆中,所以两次都打印false。

在jdk1.7种执行第一个会打印true,第二个会打印false

1.7以后版本intern()方法会把首次出现的字符串实例的引用运行时常量池中,

"计算机软件"是首次出现的字符串,会把str1的引用存入运行时常量池,所以str1.intern()返回的就是str1的引用,打印true。

"java"在创建str2对象之前已出现过,运行时常量池中已经存在,所以打印false。