6.3.2 常量池
6.3.2 常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常 量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始 的,如图6-3所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就 代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量 空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有 常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的 容量计数都与一般习惯相同,是从0开始。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class 文件的时候进行动态连接(具体见第7章)。也就是说,在Class文件中不会保存各个方法、字段最终 在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号 引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容, 在下一章介绍虚拟机类加载过程时再详细讲解。
常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量[^1],为了支持Java模块化系统 (Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。
这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位(tag,取值见表6-3中标志列),代表着当前常量属于哪种常量类型。17种常量类型所代表的具体含义如表6-3所示。
之所以说常量池是最烦琐的数据,是因为这17种常量类型各自有着完全独立的数据结构,两两之间并没有什么共性和联系,因此只能逐项进行讲解。
请读者回头看看图6-3中常量池的第一项常量,它的标志位(偏移地址:0x0000000A)是0x07,查表6-3的标志列可知这个常量属于CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的符号引用。CONSTANT_Class_info的结构比较简单,如表6-4所示。
tag是标志位,它用于区分常量类型;name_index是常量池的索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定名,本例中的name_index值(偏移地址:0x0000000B)为0x0002,也就是指向了常量池中的第二项常量。继续从图6- 3中查找第二项常量,它的标志位(地址:0x0000000D)是0x01,查表6-3可知确实是一个CONSTANT_Utf8_info类型的常量。CONSTANT_Utf8_info类型的结构如表6-5所示。
length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是: 从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示, 从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’开始到’\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。
顺便提一下,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。
本例中这个字符串的length值(偏移地址:0x0000000E)为0x001D,也就是长29个字节,往后29 个字节正好都在1~127的ASCII码范围以内,内容为“org/fenixsoft/clazz/TestClass”,有兴趣的读者可以自己逐个字节换算一下,换算结果如图6-4中选中的部分所示。
到此为止,我们仅仅分析了TestClass.class常量池中21个常量中的两个,还未提到的其余19个常量都可以通过类似的方法逐一计算出来,为了避免计算过程占用过多的版面篇幅,后续的19个常量的计算过程就不手工去做了,而借助计算机软件来帮忙完成。在JDK的bin目录中,Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap。代码清单6-2中列出了使用javap工具的- verbose参数输出的TestClass.class文件字节码内容(为节省篇幅,此清单中省略了常量池以外的信息)。笔者曾经提到过Class文件中还有很多数据项都要引用常量池中的常量,建议读者不妨在本页做个记号,因为代码清单6-2中的内容在后续的讲解之中会频繁使用到。
1 | C:\>javap -verbose TestClass |
我的效果:
1 | PS D:\Desktop\test\Java> javap -verbose .\TestClass.class |
从代码清单6-2中可以看到,计算机已经帮我们把整个常量池的21项常量都计算了出来,并且第 1、2项常量的计算结果与我们手工计算的结果完全一致。仔细看一下会发现,其中有些常量似乎从来 没有在代码中出现过,如“I”“V”“<init>
”“LineNumberTable”“LocalVariableTable”等,这些看起来在源代 码中不存在的常量是哪里来的?
这部分常量的确不来源于Java源代码,它们都是编译器自动生成的,会被后面即将讲到的字段表 (field_info)、方法表(method_info)、属性表(attribute_info)所引用,它们将会被用来描述一些不方便使用“固定字节”进行表达的内容,譬如描述方法的返回值是什么,有几个参数,每个参数的类型是什么。因为Java中的“类”是无穷无尽的,无法通过简单的无符号数来描述一个方法用到了什么类, 因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。这部分内容将在后面进一步详细阐述。最后,笔者将17种常量项的结构定义总结为表6-6。
[^1]: JDK 7时增加了前三种:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和 CONSTANT_InvokeDynamic_info。出于性能和易用性的考虑(JDK 7设计时已经考虑到,预留了17个 常量标志位),在JDK 11中又增加了第四种常量CONSTANT_Dynamic_info。本章不会涉及这4种新增 的类型,留待第8章介绍字节码执行和方法调用时详细讲解。