说到常量池,我们可以在两个地方找到常量池 ( Constant Pool )的身影,一个是在 Class字节码文件的文件结构中找到“常量池”;其次是在 JVM虚拟机 中的 方法区 内找到常量池的身影——运行时常量池~那两者到底有什么区别和关联呢?本文主要就是解决这个疑问。

Class字节码文件里的常量池

Class文件常量池又可以简单称为常量池 (Constant Pool)。当 Java文件 被编译后,其产物——字节码文件中会包含一个叫做 “常量池” 的结构
image.png
该结构里会存放编译期生成的各种字面量 ( Literal ) 符号引用 ( Symbolic References )

  • 字面量 ( Literal )
    • 在整个字节码文件中出现的用双引号括起来的 字符串字面量
    • 基本数据类型 (不包含String) 成员属性 (不包含方法里的局部变量)
    • final 类型的常量值
    • 其他,比如方法名称、类名称、类型名称、方法参数返回类型的字符串描述等等
  • 符号引用 (Symbolic References ):
    • 被模块导出或者引入的包
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符 (方法参数、返回类型)
    • 方法句柄 (方法的引用) 和方法类型
    • 动态调用点和动态常量

      符号引用可以理解为静态的内容,在运行期间这些符号引用会被转换为真正的内存地址。可以类比C语言的符号引用在链接时被翻译为真实的内存地址入口,只不过C和Java的流程不太一样而已,概念上都一样~

具体的关于 字节码文件结构,可以参考这篇文章《Class文件格式》

因为 字节码文件中的“常量池” 都还只是符号引用,是静态的,无法直接使用,所以就有了运行时常量池来存储运行时期的这些数据。

运行时常量池

每个Class的常量池信息都会在 类加载完毕后 存储至 运行时常量池,运行时常量池里存储的主要内容就是 Class信息、符号引用翻译出来的直接引用、字符串翻译成 String对象 的引用等等。其示意图如下所示:
⛲JVM - 方法区与常量池 - 图2

字符串常量池

字符串常量池是全局的,整个JVM中仅存在一份,因此也称为全局字符串常量池。它逻辑上属于方法区,但是实际位置会随着虚拟机版本变更而发生变化。能进入 字符串常量池 的 常量字符串 大部分都在类加载期间由 字节码文件中的常量池 决定了;另外一部分动态生成的字符串可以在运行期使用 String#intern() 添加到字符串常量池。
判断是否使用字符串常量池的一个依据就是 字节码指令中是否存在 ldc 指令,该指令的结构如下所示:
ldc #index
其中 #index 是指 引用字符串 在字节码文件里常量池的索引号。 ldc 指令对字符串字面值的执行语义是:到 当前类的运行时常量池 里查找该 #index 对应的项,如果该项尚未解析则解析之,并返回解析后的内容,即字符串的引用。

常量池中的 CONSTANT_String_info 这货并不是随着类加载完毕就直接可以使用的,它通常是懒加载的,在未被使用的前提下,它仅存在于运行时常量池中,以 JVM_CONSTANT_UnresolvedClass 存在;当执行了 ldc 指令后,若指定索引的字符串未被解析,那么就解析它,并将解析生成的字符串对象的引用保存到字符串常量池中

JVM 中除了字符串常量池,8种基本数据类型中除了两种浮点类型剩余的6种基本数据类型的包装类,都使用了缓冲池技术,但是 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在 [-128,127] 时才会使用缓冲池,超出此范围仍然会去创建新的对象。

字符串常量池的历史变迁

  • 在JDK6的时候,字符串常量池同运行时常量池一起存放在 方法区,即永久代

点击查看【processon】

  • 在JDK7的时候,字符串常量池挪到了堆区,剩余的仍然留在方法区,即永久代

点击查看【processon】

  • 在JDK8移除后,字符串常量池仍然在堆区,但因为永久代被删除,取而代之的是元空间,而元空间存放在直接内存中,所以剩余的方法区数据存放在直接内存中

点击查看【processon】

资料