Class 常量池
原文地址:Java 工程师成神之路
在 Java 中,常量池的概念想必很多人都听说过。这也是面试中比较常考的题目之一。在 Java 有关的面试题中,一般习惯通过 String 的有关问题来考察面试者对于常量池的知识的理解,几道简单的 String 面试题难倒了无数的开发者。所以说,常量池是 Java 体系中一个非常重要的概念。
谈到常量池,在 Java 体系中,共用三种常量池。分别是字符串常量池、Class 常量池和运行时常量池。
本文先来介绍一下到底什么是 Class 常量池。
什么是 Class 文件
在Java 代码的编译与反编译那些事儿中我们介绍过 Java 的编译和反编译的概念。我们知道,计算机只认识 0 和 1,所以程序员写的代码都需要经过编译成 0 和 1 构成的二进制格式才能够让计算机运行。
我们在《深入分析 Java 的编译原理》中提到过,为了让 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 代码:
1 | public class HelloWorld { |
通过 javac 命令生成 class 文件:
1 | 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
,可以通过
1 | javap -v HelloWorld.class |
查看常量池内容如下:
从上图中可以看到,反编译后的 class 文件常量池中共有 16 个常量。而 Class 文件中常量计数器的数值是 0011,将该 16 进制数字转换成 10 进制的结果是 17。
原因是与 Java 的语言习惯不同,常量池计数器是从 1 开始而不是从 0 开始的,常量池的个数是 10 进制的 17,这就代表了其中有 16 个常量,索引值范围为 1-16。
常量池中有什么
介绍完了什么是 Class 常量池以及如何查看常量池,那么接下来我们就要深入分析一下,Class 常量池中都有哪些内容。
常量池中主要存放两大类常量:字面量(literal)和符号引用(symbolic references)。
字面量
前面说过,Class 常量池中主要保存的是字面量和符号引用,那么到底什么字面量?
在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
以上是关于计算机科学中关于字面量的解释,并不是很容易理解。说简单点,字面量就是指由字母、数字等构成的字符串或者数值。
字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=123 这里的 a 为左值,123 为右值。在这个例子中 123 就是字面量。
1 | int a = 123; |
上面的代码事例中,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 真的运行时,需要把常量池中的常量加载到内存中。
字符串常量池
Java 中有两种方式创建一个字符串对象:
1 | String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd""; |
这两种不同的创建方法是有差别的。
- 第一种方式是在常量池中拿对象;
- 第二种方式是直接在堆内存空间创建一个新的对象。
而第一种是我们比较常用的做法,这种形式叫做”字面量”。
在 JVM 中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池。
当代码中出现双引号形式(字面量)创建字符串对象时,JVM 会先对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;否则,创建新的字符串对象,然后将这个引用放入字符串常量池,并返回该引用。
这种机制,就是字符串驻留或池化。
记住一点:只要使用 new 方法,便需要创建新的对象。
String s1 = new String("abc")
这段代码一共构建了几个对象?
1 | String s1 = new String("abc"); |
这段代码将创建 1 或 2 个字符串对象。
- 如果池中已存在字符串常量
abc
,则只会在堆空间创建一个字符串对象 s1,s1 内部的char value[]
则指向常量池中的abc
。 - 如果池中没有字符串常量
abc
,那么它将首先在池中创建abc
对象,然后在堆空间中创建 s1 对象,s1 指向堆中 new 的对象,而 s1 内部的char value[]
则指向常量池中的abc
。因此将创建总共 2 个字符串对象。
所以 s1 指向堆内存,s2 指向常量池,但是 s1 的value[]
和 s2 的value[]
指向的是常量池中的同一个对象。所以s1 == s2
为 false,s1.equals(s2)
为 true。
8 种基本类型的包装类和常量池
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。
而两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。
字符串常量池的位置
在目前我所看到的文章中,有些表示字符串常量池物理上属于堆内存,但是逻辑上是属于方法区的。而运行时常量池则在 jdk1.7 中未从永久代移出,所以在 jdk1.8 中由元空间即堆外内存实现。
也有的说法是运行时常量池也由堆来实现,元空间只存储有类的元信息。
希望有大佬告诉我具体解答。
运行时常量池
原文地址:运行时常量池
运行时常量池( Runtime Constant Pool)是每一个类或接口的常量池( Constant_Pool)的运行时表示形式。
它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表( SymbolTable)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
以上,是 Java 虚拟机规范中关于运行时常量池的定义。
运行时常量池中常量的来源
运行时常量池中包含了若干种不同的常量:
- 编译期可知的字面量和符号引用(来自 Class 常量池)
- 运行期解析后可获得的常量(如 String 的 intern 方法)
所以,运行时常量池中的内容包含:Class 常量池中的常量、字符串常量池中的内容。
运行时常量池、Class 常量池、字符串常量池的区别与联系
虚拟机启动过程中,会将各个 Class 文件中的常量池载入到运行时常量池中。
所以, Class 常量池只是一个媒介场所。在 JVM 真的运行时,需要把常量池中的常量加载到内存中,进入到运行时常量池。
字符串常量池可以理解为运行时常量池分出来的部分。加载时,对于 class 的静态常量池,如果字符串会被装到字符串常量池中。
参考
Java 工程师成神之路 >Java 内存区域详解 >字符串常量池和运行时常量池是在堆还是在方法区? >JDK1.8 关于运行时常量池, 字符串常量池的要点 >Java 方法区、永久代、元空间、常量池详解