说到常量池,我们可以在两个地方找到常量池 ( Constant Pool )的身影,一个是在 Class字节码文件的文件结构中找到“常量池”;其次是在 JVM虚拟机 中的 方法区 内找到常量池的身影——运行时常量池~那两者到底有什么区别和关联呢?本文主要就是解决这个疑问。
Class字节码文件里的常量池
Class文件常量池又可以简单称为常量池 (Constant Pool)。当 Java文件 被编译后,其产物——字节码文件中会包含一个叫做 “常量池” 的结构:
该结构里会存放编译期生成的各种字面量 ( Literal ) 和 符号引用 ( Symbolic References ):
- 字面量 ( Literal )
- 在整个字节码文件中出现的用双引号括起来的 字符串字面量
- 基本数据类型 (不包含String) 成员属性 (不包含方法里的局部变量)
final
类型的常量值- 其他,比如方法名称、类名称、类型名称、方法参数返回类型的字符串描述等等
- 符号引用 (Symbolic References ):
- 被模块导出或者引入的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符 (方法参数、返回类型)
- 方法句柄 (方法的引用) 和方法类型
- 动态调用点和动态常量
符号引用可以理解为静态的内容,在运行期间这些符号引用会被转换为真正的内存地址。可以类比C语言的符号引用在链接时被翻译为真实的内存地址入口,只不过C和Java的流程不太一样而已,概念上都一样~
具体的关于 字节码文件结构,可以参考这篇文章《Class文件格式》
因为 字节码文件中的“常量池” 都还只是符号引用,是静态的,无法直接使用,所以就有了运行时常量池来存储运行时期的这些数据。
运行时常量池
每个Class的常量池信息都会在 类加载完毕后 存储至 运行时常量池,运行时常量池里存储的主要内容就是 Class信息、符号引用翻译出来的直接引用、字符串翻译成 String对象 的引用等等。其示意图如下所示:
字符串常量池
字符串常量池是全局的,整个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的时候,字符串常量池同运行时常量池一起存放在 方法区,即永久代
- 在JDK7的时候,字符串常量池挪到了堆区,剩余的仍然留在方法区,即永久代
- 在JDK8移除后,字符串常量池仍然在堆区,但因为永久代被删除,取而代之的是元空间,而元空间存放在直接内存中,所以剩余的方法区数据存放在直接内存中