在学习JVM相关知识时,尤其时学习JVM内存区域相关内容时,经常遇到各种常量池的概念,每次都是遇到都是模棱两可,让我仔细说一说,可能有说不清楚,其实我在写上一篇文章——[JVM面试题详解系列——JVM内存区域详解](JVM面试题详解系列——JVM内存区域详解 - 掘金 (juejin.cn)),就想介绍这个问题。但是这个概念我认为还是很重要而且很难区分,所以我想单独写一篇文章详细介绍一下。 这些都是我看了许多资料加上我的理解总结的,难免会有理解不到位的,如果由错误的地方麻烦大家在评论区@我,提出指正意见。
字符串常量池(string pool)
字符串常量池是JVM为了提升性能和减少内存消耗针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着JDK版本的不同而位置不同。在JDK1.6及之前,字符串常量池的位置在永久代中,此时字符串常量池中存储的是字符串对象;在JDK1.7时,字符串常量池的位置从永久代移动到了Java堆中,此时,字符串常量池存储的就是字符串对象的引用,具体的实例对象是在堆中开辟的一块空间存放的;在JDK1.8及之后,永久代被元空间取代了。
HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 本质上就是一个HashSet ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
StringTable 中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
注意:在JDK1.7时,静态变量和字符串常量池一起从永久代中移动到了Java堆中。
JDK 1.7 为什么要将字符串常量池移动到堆中?
主要是因为永久代的GC回收效率太低,只有在整堆收集(Full GC)的时候才会被执行GC,而Java程序中通常由大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
class文件的常量池(class constant pool)
class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.
字面量
字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用
符号引用是一组符号来描述所引用的目标,属于编译原理方面的概念,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Full Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法的句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
很多资料只提到了2—4,大家可以重点记一下。
直接引用
在JVM 类加载过程中,解析阶段,Java虚拟机将常量池内的符号引用替换为直接引用。直接引用可以帮助程序直接定位到所需的对象。
直接引用一般为下面三类:
- 直接指向目标的指针
- 相对偏移量
- 一个能够直接定位到目标的句柄
句柄
句柄是一个是用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量
偏移量
计算机汇编语言,是指把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。
直接引用适合虚拟机的布局相关,同一个符号引用在不同的虚拟机上翻译出来的直接引用一般会不一致。如果有了直接引用,那么引用目标必定已经被加载到了内存当中。
运行时常量池(runtime constant pool)
当Java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中所引用的是一致的。
运行时常量池相对于class文件的常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
总结:运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与字符串常量池中的引用值保持一致。