首页 - 技术 -  Java 常量池

Java 常量池

2023-09-23 12:40

常量池分为 Class 常量池常量池、运行时常量池、字符串常量池。

1、 Class 常量池常量池(静态常量池)

Java 文件被编译成 Class 文件,Class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是 Class 常量池,Class 常量池是当 Class 文件被 Java 虚拟机加载进来后存放各种字面量 (Literal)和符号引用

字面量相当于 Java 语言层面常量的概念,如文本字符串、基础数据、声明为 final 的常量等;符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称描述符、方法名称描述符。类的加载过程中的链接部分的解析步骤就是把符号引用替换为直接引用,即把那些描述符(名字)替换为能直接定位到字段、方法的引用或句柄(地址)。

2、运行时常量池

运行时常量池是方法区的一部分。运行时常量池是当 Class 文件被加载到内存后,Java虚拟机会将 Class 文件常量池里的内容转移到运行时常量池里,即编译期间生成的字面量、符号引用(运行时常量池也是每个类都有一个)。一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储到运行时常量池中。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中

(1)方法区的 Class 文件信息,Class 常量池和运行时常量池的三者关系:

(2)运行时常量池在 JDK1.6、JDK1.7 和 JDK 8 的变化

1)在 JDK1.7 之前运行时常量池存放在方法区,逻辑包含字符串常量池,存放字面量和符号引用,此时 hotspot 虚拟机对方法区的实现为永久代。
2)在 JDK1.7 中字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代。
3)在 JDK1.8 中,hotspot 移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)

3、字符串常量池

字符串常量池又称为字符串池,全局字符串池,英文也叫 String Pool。JVM 为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,这就是字符串常量池。字符串常量池由 String 类私有的维护。
全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的字符串值存到 string pool 中(记住:string pool中存的是字符串值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)

(1)字符串常量池的位置变化

1) 在 JDK7 之前字符串常量池是在永久代里边的,但是在 JDK7 中,把字符串常量池移到了堆里边

2) 在 JDK8 中,把字符串常量池仍在堆里边

(2)两种创建字符串对象不同方式的比较

1)采用字面值的方式创建字符串对象

public class Str {public static void main(String[] args) {String str1="aaa";String str2="aaa";System.out.println(str1==str2);   }
}// 运行结果:
// true

采用字面值的方式创建一个字符串时,JVM 首先会去字符串池中查找是否存在 "aaa" 这个对象,如果不存在,则在字符串池中创建 "aaa" 这个对象,然后将池中 "aaa" 这个对象的引用地址返回给字符串常量 str,这样 str 会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中 "aaa" 这个对象的地址返回,赋给字符串常量
对于上述的例子:这是因为,创建字符串对象 str2 时,字符串池中已经存在 “aaa” 这个对象,直接把对象 “aaa” 的引用地址返回给 str2,这样 str2 指向了池中 “aaa” 这个对象,也就是说 str1 和 str2 指向了同一个对象,因此语句 System.out.println(str1== str2) 输出:true。

2)采用 new 关键字新建一个字符串对象

public class Str {public static void main(String[] args) {String str1=new String("aaa");String str2=new String("aaa");System.out.println(str1==str2);}
}// 运行结果:
// false

采用 new 关键字新建一个字符串对象时,JVM 首先在字符串常量池中查找有没有 "aaa" 这个字符串对象,如果有,则不在池中再去创建 "aaa" 这个对象了,直接在堆中创建一个 "aaa" 字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用 str1,这样,str1 就指向了堆中创建的这个 "aaa" 字符串对象;如果没有,则首先在字符串常量池池中创建一个 "aaa" 字符串对象,然后再在堆中创建一个 "aaa" 字符串对象,然后将堆中这个 "aaa" 字符串对象的地址返回赋给 str1 引用,这样,str1 指向了堆中创建的这个 "aaa" 字符串对象
对于上述的例子:因为,采用new关键字创建对象时,每次new出来的都是一个新的对象,也即是说引用str1和str2指向的是两个不同的对象,因此语句
System.out.println(str1 == str2)输出:false

(3)字符串池的实现

由于字符串池是虚拟机层面的技术,所以在 String 的类定义中并没有类似 IntegerCache 这样的对象池,String 类中提及缓存/池的概念只有 intern() 这个方法。

/**
* 返回一个标准的字符串对象。
*
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
*
* 当 intern 方法被调用,若池中包含一个被{@link #equals(Object)}方法认定为和该
* String对象相等的String,那么返回池中的String,否则,将该String对象添加到池中
* 并返回它的引用。
*
* All literal strings and string-valued constant expressions are
* interned.
*/ 
public native String intern(); 

intern() 是一个native 的方法,那么说明它本身并不是由 Java 语言实现的,而是通过 jni (Java Native Interface)调用了其他语言(如C/C++)实现的一些外部方法。
大体实现:Java 调用 c++ 实现的 StringTable 的 intern() 方法。StringTable 的 intern() 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容,默认大小是1009。
字符串池(String pool)实际上是一个 HashTable。Java 中 HashMap 和 HashTable 的原理大同小异,将字符串池看作哈希表更便于我们套用学习数据结构时的一些知识。比如解决数据冲突时,HashMap 和 HashTable 使用的是开散列(或者说拉链法)。

(5)字符串池的优缺点

字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。

总结

1、全局字符串常量池在每个 VM 中只有一份,存放的是字符串常量的引用值。
2、class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放的是常量的符号引用。
3、运行时常量池是在类加载完成之后,将每个 class 常量池中的符号引用值转存到运行时常量池中,也就是说,每个 class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。