在 Java 中, 常量池的概念想必很多人都听说过. 这也是面试中比较常考的题目之一. 在 Java 有关的面试题中, 一般习惯通过 String 的有关问题来考察面试者对于常量池的知识的理解, 几道简单的 String 面试题难倒了无数的开发者. 所以说, 常量池是 Java 体系中一个非常重要的概念.
谈到常量池, 在 Java 体系中, 共用三种常量池. 分别是字符串常量池, Class 常量池和运行时常量池.
本文是《好好说说 Java 中的常量池》系列的第一篇, 先来介绍一下到底什么是 Class 常量池.
什么是 Class 文件
在 Java 代码的编译与反编译那些事儿 https://mp.weixin.qq.com/s/H-zuj1PDQAPlhRIOPhsbgw 中我们介绍过 Java 的编译和反编译的概念. 我们知道, 计算机只认识 0 和 1, 所以程序员写的代码都需要经过编译成 0 和 1 构成的二进制格式才能够让计算机运行.
我们在深入分析 Java 的编译原理 https://mp.weixin.qq.com/s/XH-JajAne0O7_yCYE5wBbg 中提到过, 为了让 Java 语言具有良好的跨平台能力, Java 独具匠心的提供了一种可以在所有平台上都能使用的一种中间代码 -- 字节码(ByteCode).
有了字节码, 无论是哪种平台(如 Windows,Linux 等), 只要安装了虚拟机, 都可以直接运行字节码.
同样, 有了字节码, 也解除了 Java 虚拟机和 Java 语言之间的耦合. 这话可能很多人不理解, Java 虚拟机不就是运行 Java 语言的么? 这种解耦指的是什么?
其实, 目前 Java 虚拟机已经可以支持很多除 Java 语言以外的语言了, 如 Groovy,JRuby,Jython,Scala 等. 之所以可以支持, 就是因为这些语言也可以被编译成字节码. 而虚拟机并不关心字节码是有哪种语言编译而来的.
Java 语言中负责编译出字节码的编译器是一个命令是 javac.
javac 是收录于 JDK 中的 Java 语言编译器. 该工具可以将后缀名为. java 的源文件编译为后缀名为. class 的可以运行于 Java 虚拟机的字节码.
如, 我们有以下简单的 HelloWorld.java 代码:
- public class HelloWorld {
- public static void main(String[] args) {
- String s = "Hollis";
- }
- }
通过 javac 命令生成 class 文件:
javac HelloWorld.java
生成 HelloWorld.class 文件:
如何使用 16 进制打开 class 文件: 使用 VIM test.class , 然后在交互模式下, 输入:%!xxd 即可.
可以看到, 上面的文件就是 Class 文件, Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息.
要想能够读懂上面的字节码, 需要了解 Class 类文件的结构, 由于这不是本文的重点, 这里就不展开说明了.
读者可以看到, HelloWorld.class 文件中的前八个字母是 cafe babe, 这就是 Class 文件的魔数(Java 中的 "魔数")
我们需要知道的是, 在 Class 文件的 4 个字节的魔数后面的分别是 4 个字节的 Class 文件的版本号 (第 5,6 个字节是次版本号, 第 7,8 个字节是主版本号, 我生成的 Class 文件的版本号是 52, 这时 Java 8 对应的版本. 也就是说, 这个版本的字节码, 在 JDK 1.8 以下的版本中无法运行) 在版本号后面的, 就是 Class 常量池入口了.
Class 常量池
Class 常量池可以理解为是 Class 文件中的资源仓库. Class 文件中除了包含类的版本, 字段, 方法, 接口等描述信息外, 还有一项信息就是常量池 (constant pool table), 用于存放编译器生成的各种字面量(Literal) 和符号引用(Symbolic References).
由于不同的 Class 文件中包含的常量的个数是不固定的, 所以在 Class 文件的常量池入口处会设置两个字节的常量池容量计数器, 记录了常量池中常量的个数.
当然, 还有一种比较简单的查看 Class 文件中常量池的方法, 那就是通过 javap 命令. 对于以上的 HelloWorld.class, 可以通过
javap -v HelloWorld.class
查看常量池内容如下:
从上图中可以看到, 反编译后的 class 文件常量池中共有 16 个常量. 而 Class 文件中常量计数器的数值是 0011, 将该 16 进制数字转换成 10 进制的结果是 17.
原因是: 与 Java 的语言习惯不同, 常量池计数器是从 1 开始而不是从 0 开始的, 常量池的个数是 十进制的 17, 这就代表了其中有 16 个常量, 索引值范围为 1-16.
常量池中有什么
介绍完了什么是 Class 常量池以及如何查看常量池, 那么接下来我们就要深入分析一下, Class 常量池中都有哪些内容.
常量池中主要存放两大类常量: 字面量 (literal) 和符号引用(symbolic references).
字面量
前面说过, 运行时常量池中主要保存的是字面量和符号引用, 那么到底什么字面量?
在计算机科学中, 字面量 (literal) 是用于表达源代码中一个固定值的表示法(notation). 几乎所有计算机编程语言都具有对基本值的字面量表示, 诸如: 整数, 浮点数以及字符串; 而有很多也对布尔类型和字符类型的值也支持字面量表示; 还有一些甚至对枚举类型的元素以及像数组, 记录和对象等复合类型的值也支持字面量表示法.
以上是关于计算机科学中关于字面量的解释, 并不是很容易理解. 说简单点, 字面量就是指由字母, 数字等构成的字符串或者数值.
字面量只可以右值出现, 所谓右值是指等号右边的值, 如: int a=123 这里的 a 为左值, 123 为右值. 在这个例子中 123 就是字面量.
int a = 123;String s = "hollis";
上面的代码示例中, 123 和 hollis 都是字面量.
本文开头的 HelloWorld 代码中, Hollis 就是一个字面量.
符号引用
常量池中, 除了字面量以外, 还有符号引用, 那么到底什么是符号引用呢.
符号引用是编译原理中的概念, 是相对于直接引用来说的. 主要包括了以下三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
这也就可以印证前面的常量池中还包含一些 com/hollis/HelloWorld,main,([Ljava/lang/String;)V 等常量的原因了.
Class 常量池有什么用
前面介绍了这么多, 关于 Class 常量池是什么, 怎么查看 Class 常量池以及 Class 常量池中保存了哪些东西. 有一个关键的问题没有讲, 那就是 Class 常量池到底有什么用.
首先, 可以明确的是, Class 常量池是 Class 文件中的资源仓库, 其中保存了各种常量. 而这些常量都是开发者定义出来, 需要在程序的运行期使用的.
在《深入理解 Java 虚拟》中有这样的表述:
Java 代码在进行 Javac 编译的时候, 并不像 C 和 C++ 那样有 "连接" 这一步骤, 而是在虚拟机加载 Class 文件的时候进行动态连接. 也就是说, 在 Class 文件中不会保存各个方法, 字段的最终内存布局信息, 因此这些字段, 方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址, 也就无法直接被虚拟机使用. 当虚拟机运行时, 需要从常量池获得对应的符号引用, 再在类创建时或运行时解析, 翻译到具体的内存地址之中. 关于类的创建和动态连接的内容, 在虚拟机类加载过程时再进行详细讲解.
这段话, 看起来很绕, 不是很容易理解. 其实他的意思就是: Class 是用来保存常量的一个媒介场所, 并且是一个中间场所. 在 JVM 真的运行时, 需要把常量池中的常量加载到内存中.
至于到底哪个阶段会做这件事情, 以及 Class 常量池中的常量会以何种方式被加载到具体什么地方, 会在本系列文章的后续内容中继续阐述. 欢迎关注我的博客(https://www.hollischuang.com) 和公众号(Hollis), 即可第一时间获得最新内容.
另外, 关于常量池中常量的存储形式, 以及数据类型的表示方法本文中并未涉及, 并不是说这部分知识点不重要, 只是 Class 字节码的分析本就枯燥, 作者不想在一篇文章中给读者灌输太多的理论上的内容. 感兴趣的读者可以自行 Google 学习, 如果真的有必要, 我也可以单独写一篇文章再深入介绍.
来源: http://zhuanlan.51cto.com/art/201810/585787.htm