一、 静态常量池与运行时常量池
1.1 静态常量池定义
静态常量池也可以称为Class常量池,.java
文件经过编译后生成的.class
文件,每个.class
文件里面都包含了一个常量池,因为这个常量池是在Class文件里面定义的,也就是.java
文件编译后不能修改,所以称之为静态常量池。
在程序运行的时候,JVM会把 .class文件加载进内存,然后在解析阶段会把Class中的符号引用转化成直接引用。这些直接引用就是在内存的运行时常量池中,运行时常量池除了包含类的相关信息外,还有字符串常量池等。
1.2 区别
运行时常量池与静态常量池最大的区别就在于运行时常量池里面的数据会随着程序的运行有所变动。
二、字面量与字符串常量池
2.1定义
2.1.1字面量
字面量是指由字母、数字等构成的字符串或者数值常量,给变量赋予的一个常量值。
2.2.2字符串常量
字符串常量池是Java运行时常量池比较特殊的一块空间(字符串属于引用类型,但是在Java中字符串的使用频率与基本数据类型差不多,甚至更高。如果与其他的引用类型一样,每个字符串都分配大量的额外空间,会导致时间和空间的双重浪费,同时也会极大地影响程序的性能。)
为字符串开启了一块独立的内存空间,字符串常量池,类似于缓冲区。创建字符串常量时,首先查询字符串常量池中是否存在该字符串(equal)。如果存在该字符串,直接返回引用实例;如果不存在,实例化该字符串并放入常量池中
2.2 字符串创建与内存分配
2.2.1 直接赋值
String a = "lizhi";
字符串引用只会在常量池中。创建对象a的时候,JVM会先去常量池中通过equal(key)方法,判断是否有相同的对象。如果有,则直接返回该对象在字符串常量池中的引用;如果没有,则会在常量池中创建一个新对象,再返回引用。
2.2.2 new关键字
String a = new String("lizhi"); // a指向堆中的引用
字符串常量池和堆内存都会有这个对象,没有就创建,最后返回的是堆内存中的对象引用。
因为有lizhi这个字面量,先去检查字符串常量池中是否存在该字符串,如果存在,就直接去堆内存创建一个字符串对象,内容为lizhi , 最后将堆内存的字符串引用返回。如果不存在,就直接先在字符串常量池中创建一个字符串对象,然后再去堆内存中创建一个字符串对象lizhi;
2.2.3 intern方法
String a = new String("lizhi");
String b = a.intern();System.out.println(a == b); // 结果为false
调用intern()方法(String类的本地方法),会判断字符串常量池中是否有该对象的引用,通过equal()方法判断,如果存在就返回字符串常量池中的对象引用。
如果不存在,就把当前字符串对象直接添加到字符串常量池中,是指把堆中对象的引用添加到常量池,并不是在常量池中再创建一个该字符串的对象,然后返回该对象的引用。
2.3 String
以及字符串常量池的理解
2.3.1 编译合并(字符串常量池的引用)
String a = "lizhi";
String b = "lizhi";
String c = "li" + "zhi";
// 结果均为true
System.out.println(a == b);
System.out.println(a == c);
对象a和对象b,都是直接通过字面量来创建字符串对象,所以这两个对象对应的都是字符串常量池的对象引用。
而对象c,虽然是通过+进行连接,但相连的两个字符串都是字面量,在编译期是可以确定的,所以编译器会将这两个字面量进行合并。即在Class文件的常量池中,就只有字面量lizhi,而没有li和zhi这两个字面量。
综上所述,上面三个对象都是用的是字符串常量池的引用
2.3.2 编译期无法合并引用
String a = "lizhi";
String b = new String("lizhi");
String c = "li" + new String("zhi");
// 结果均为false
System.out.println(a == b);
System.out.println(a == c);
System.out.println(b == c);
对象a的引用是字符串常量池中的引用,对象b是堆内存中的对象引用,而对象c由于new String()创建的字符串不是常量或字面量,在编译期间无法确定,所以new String()创建的字符串不会放入到常量池,自然也就没法在编译期进行合并了,然后在运行时通过append在堆内存创建一个新的对象。
再比如下面的例子:
String a = "lizhi";
String b = "li";
String c = b + "zhi";
// 结果为false
System.out.println(a == c);
由于在编译期对象b是符号引用,无法确定,所以对象c也没法在编译期进行合并,只能运行时在堆中创建li对象然后通过append拼接zhi。
但是,如果对象b是一个常量,在编译期会被解析成常量值的一个本地拷贝存储到常量池中,比如下面的例子:
String a = "lizhi";
final String b = "li";
String c = b + "zhi";
// 结果为true
System.out.println(a == c);
与上面例子唯一的不同就在于对象b变成了一个常量,这在编译期是可以确定。
三、包装类与对象池
Java中八种基本数据类型的包装类基本都实现了缓存技术,目的也是为了避免重复创建过于的引用,这其中就包含Byte、Short、Integer、Long、Character、Boolean,而另外两种包装类型Float和Double则没有实现常量池技术,原因在于这两种数据类型的数值是很随意的,就算有常量池命中率也不会高,还浪费额外的堆内存。
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // 结果为trueInteger c = 128;
Integer d = 128;
System.out.println(c == d); // 结果为falseInteger e = new Integer(127);
Integer f = new Integer(127);
System.out.println(e == f); // 结果为false
对于上面的结果,这种通过通过字面量的方式赋值,其实在C++底层都是调用Integer.valueOf(127),而这里面就是用到了Integer的缓存池,源码如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
最小值为-128,最大值默认为127,但可以通过属性进行配置,然后在IntegerCache的静态代码段中,为-128~127的所有整数生成一个Integer对象,然后添加到cache数据中,当调用Integer.valueof()时会判断数值是否在这个区间内,如果在就直接返回已经缓存好的对象,如果不再就直接新创建一个Integer对象。
而对于通过new关键字创建的对象,自然不会使用缓存池中的对象。
原文链接:https://blog.csdn.net/sermonlizhi/article/details/124945205