首先,我们来看看常量池的概念,常量池可以分成3类:
1.静态常量池:也就是class文件中的常量池,一般用来存放class文件中定义的一些常量,包括类和接口的全限定名,字段的名称和描述符以及方法和名称和描述符。
2.字符串常量池:即class文件中定义的String类型,这个常量池就存在与静态常量池中。
3.运行时常量池:我们平时所说的常量池就是这个了,它存放在方法区中,当所有的class文件都加载完成之后,会将多有的常量都放入其中,以供开发使用。
那么,哪些数据会被存放到常量池中呢?除了上面提到的String类型,8中基本数据类型中有6中都会存放如常量池中,下面我们来看一段代码:
public class ConstantTest {
public static void main(String[] args) {
//验证Integer类型
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1==i2);//true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3==i4);//false
//验证Long类型
Long l1 = 127L;
Long l2 = 127L;
System.out.println(l1==l2);//true
Long l3 = 128L;
Long l4 = 128L;
System.out.println(l3==l4);//false
//验证Byte类型
Byte b1 = 127;
Byte b2 = 127;
System.out.println(b1==b2);//true
//验证short类型
Short s1 = 127;
Short s2 = 127;
System.out.println(s1==s2);//true
Short s3 = 128;
Short s4 = 128;
System.out.println(s3==s4);//false
//验证Character类型
Character c1 = 'a';
Character c2 = 'a';
System.out.println(c1==c2);//true
Character c3 = '你';
Character c4 = '你';
System.out.println(c3==c4);//false
//验证boolean类型
Boolean bo1 = true;
Boolean bo2 = true;
System.out.println(bo1==bo2);//true
//验证double类型
Double d1 = 1.2;
Double d2 = 1.2;
System.out.println(d1==d2);//false
//验证float类型
Float f1 = 1.2f;
Float f2 = 1.2f;
System.out.println(f1==f2);//false
}
}
通过以上的代码和运行结果,我们可以知道,8种数据类型的包装类中,除了Double和Float,其它6种都会进入常量池,但是数值范围在-128到127之间。
下面我们再来看看String类型:
public class ConstantTest {
public static void main(String[] args) {
String s1 = "java";
String s2 = new String("java");
}
}
这两行代码都是将”java“这个字符串赋值,但是在内存空间的角度来看,它们是有区别的,看下面这张图:
图中的两条绿线比较好理解,就是直接赋值的时候,是去常量池中查看是否有该对象,如果没有,就创建并返回其地址引用,如果有,就直接返回地址引用,而通过new的方法创建的String对象是存放在堆内存种的,值得注意的是那条红线,在执行new String的时候,也会去常量池中查看该字符串是否已经存在,如果不存在,那么就在常量池中创建一个。所以如果问new String("java")这句代码产生了几个对象,如果常量池中已经有这个对象了,那么只会产生一个,如果没有,那么会产生两个。
我们再来看看下面这段代码:
public class ConstantTest {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "world";
String s3 = "hello"+"world";
String s4 = s1+s2;
String s5 = "helloworld";
System.out.println(s3 == s5);//true
System.out.println(s4 == s5);//false
}
}
为什么会出现这种结果呢?我们来分析一下,前两行代码执行之后,会向常量池中添加"hello"和 "world"这两个常量,由于jvm的编译时优化,当两个常量进行相加的时候,会将这个组成的新的常量添加到常量池中,并将引用返回,所以第三行代码就是向常量池中添加了"helloworld",并将地址值赋给 s3,所以第一个为true,那么第二个为什么为false呢?因为第四行代码是两个变量相加,变量具有不确定性,所以jvm的编译优化不会起作用,所以第二个是false。
再来说说常量池的位置,在jdk1.6及之前的版本中,常量池位于perm区,也就是我们常说的方法区,这个区和堆是没有关系的,它们是分开的,而到了1.7版本之后,常量池就转放到heap中了,而且值得注意的是,在1.8版本后,perm区也被取消了,取代的是元空间(metaspace)。
下面这段代码是在jdk1.8环境下运行的:
public class InternTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < 500000000; i++) {
String intern = String.valueOf(i).intern();
list.add(intern);
}
}
}
我们将jvm堆内存的大小设置小一点:
-Xmx1m -Xms1m -XX:-UseGCOverheadLimit
这里注意一定要使用list将数据添加进去,否则会由于GC而不会出现内存溢出,运行结果如下
发现是堆内存溢出了,说明在1.8的时候,常量池已经被移到了heap中。
到这里,常量池的基本内容就差不多了,不过既然是运行时常量池,就必定牵扯到动态的向其中添加数据,开发中常见的方法就是String的intern方法,想要深入了解这个方法,可参考String的intern方法的深入分析