概述
1. 常量池在内存中的位置:
- 在JDK1.6中,方法区是以永久代的方式实现(HotSpot),常量池是方法区的一部分。
- 在JDK1.7中,方法区合并到堆内存中,常量池可以说在堆内存中。
- 在JDK8中,方法区又被剥离出来,只不过实现方式不是永久代,此时的方法区叫元数据区,常量池也就在元数据区。
2. 常量池的概念以及作用
- 通常来讲,所有变量(成员变量和局部变量),包括基本类型和引用类型,他们都存在虚拟机栈中,包括变量类型、变量名称、和变量值。对于基本类型来说,值就是具体的值;而对于引用类型来说,值是指对象实例在堆内存中对应的地址。
- 对于引用类型的数据,如果没有常量池,那么就会反复在堆内存中创建和销毁值相同的对象,这样有损系统性能。相比之下,基本类型的变量就不会有这样的弊端,所以一般不会放到常量池中,直接在栈中操作即可。
- 常量池的作用是避免频繁地创建和销毁值相同的对象,节省内存空间,节省运行时间。比如,需要已存在的字符串,直接从常量池中取即可,无需重新创建。
- 常量池:六大基本类型对应的包装类的常量池、String字符串常量池。
基本类型包装类常量池
首先,基本数据类型不会用到常量池,因为一般的运算、判断和赋值都是对值的操作,直接在虚拟机栈中进行,不涉及到对象,也就与堆内存无关(也就涉及不到对象的创建以及垃圾回收),不会对系统性能造成太大的影响。
- Java中的Byte,Short,Integer,Long,Character,Boolean六种包装类实现了常量池技术。默认只有数值处在[-128,127]这个范围的包装类对象能存到常量池当中,超过这个范围的对象都要在堆内存中创建。利用new创建的包装类对象仍然存在堆内存中。
// 在[-128,127]这个范围
Integer i1 = 40;
Integer i2 = 40;
System.out.println(i1==i2);//输出TRUE
// 不在上述范围
Integer i3 = 400;
Integer i4 = 400;
System.out.println(i3==i4); // false
- 两种浮点数类型的包装类Float、Double不支持常量池技术。
Double i1=1.2;
Double i2=1.2;
System.out.println(i1==i2);//输出false
- 包装类对象不支持算术运算(+、-、*、/、%),包装类对象在进行算术运算时会自动拆箱成基本数据类型,不再是包装类对象;包装类对象与基本数据类型进行算术和比较运算时也会自动拆箱为为基本数据类型。
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2)); // true
System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); // true
System.out.println("i1=i4 " + (i1 == i4)); // false
System.out.println("i4=i5 " + (i4 == i5)); // false
// i4,i5,i6三个对象都自动拆箱为基本数据类型,不再是对象,而是数值之间的比较。
System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // true
System.out.println("40=i5+i6 " + (40 == i5 + i6)); // true
字符串常量池
- String str1 = "a"; 用引号直接初始化的字符串str1属于字符串常量。编译期就已经放入到常量池中,str1是一个引用,str1的值是“a”在常量池中的地址。
- String str2 = new String("a"); 此时的str2是字符串变量,其指向的对象实例在堆内存中;指向的字符串对象也可以变。类加载和运行时都会处理上述语句。
- 类加载时,先判断常量池中是否已经有“a”,如果有,则什么都不做;如果没有,则创建一个“a”放在常量池中。
- 运行时,执行到new这个语句,是将常量池中的实例复制一份放到堆内存中,也无需重新创建,但str2的值是实例在堆内存中的地址。虽然不是创建了两个对象,但是确实是新增了两个新的对象。
- 上述涉及到类加载的过程未必准确,但结果相同,变量指向的是堆内存中的对象。
- 出现字符串连接符“+”的两种情况:
1. 只有包含双引号" "指定的字符串 (都在pool中)
// 在编译期间就能确定"aa"这样一个字符串对象,所以是放到常量池中,但常量池中没有"a"
String str1 = "a" + "a";
2. 不全是双引号" "指定的字符串进行连接(不都在pool中)
String str1 = new String("b");
String str2 = "c" + str1; // 在编译期间不能确定,运行期间存到堆内存中
String str3 = new String("c");
String str4 = str1 + str3; // 在编译期间不能确定,运行期间存到堆内存中
System.out.println(str2==str4) // false 都在堆内存中,地址肯定不同
示例:final修饰的变量编译器已确定,对象存在常量池中
String s0 = "111"; //pool
String s1 = new String("111"); //heap
final String s2 = "111"; //pool 编译器已确定
String s3 = "sss111"; //pool
String s4 = "sss" + "111"; //pool
String s5 = "sss" + s0; //heap
String s6 = "sss" + s1; //heap
String s7 = "sss" + s2; //pool
String s8 = "sss" + s0; //heap
System.out.println(s3 == s4); //true
System.out.println(s3 == s5); //false
System.out.println(s3 == s6); //false
System.out.println(s3 == s7); //true
System.out.println(s5 == s6); //false
System.out.println(s5 == s8); //false
判断原则(指向常量池还是堆内存)
1. 编译期间可以创建的对象放在常量池中。
String str1 = "xyz";
Integer a = 1;
2. 运行期间用new创建的对象放在堆内存中。
String str1 = new String("aaa");
Integer a = new Integer(2);
3. 存放位置不同的两个对象对应的引用一定不相等。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); // false
4. 运行时常量池具有动态性,即运行期间也可以向常量池中添加新的常量。
- 被开发人员利用最多的是String类的intern()方法。
// 动态性
// 常量池中本没有“abc”,程序执行以下语句时,会先在常量池中创建一个“abc”对象
// 然后把对象的引用(地址)返回给str1。此时str1的值是对象引用。
String str1 = "abc".intern();
// 常量池中已经有“xyz”对象了,那么无需创建,直接将对象返回给str3,此时str3的值是对象,而非引用。
String str2 = "xyz";
String str3 = str2.intern();
- 其次通过new创建的对象,也都会在类加载时先创建一份保存到常量池中,运行期间遇到的new相同字符串的语句时,都是直接从常量池中复制一份到堆内存中,这样也可以在一定程度上减轻创建相同内容的对象的消耗。
5. 如果指向的对象都在常量池,那么指向的一定是同一个对象,即str1=str2 (值相同,地址相同)
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); // true
6. 除了5以外的两种情况,两个对象引用的值都不相等。
// 情况1:一个在常量池中,一个在堆内存中
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); // false
// 情况2:两个都在堆内存中
String str3 = new String("xyz");
String str4 = new String("xyz");
System.out.println(str3==str4); //false