之前学习多线程的时候接触到javap反汇编指令,其中javap -verbose指令能够把Class文件中的内容详细输出出来,其中不仅仅包括反汇编后的虚拟机指令,还包括了常量池中的内容。当时还不太明白反汇编后的常量池内容含义就放了一段时间,现在又遇到这个知识点就写一篇笔记备忘。
注:本篇文章中会使用到javap指令和VSCode的Hexdump插件。
常量池
常量池是Class文件中占用空间最大的也是Class文件中第一个出现的项目,由于常量池内的常量数量不确定,因此常量池内的第一个数据是一个u2类型的无符号数,其含义是常量池的容量。值得注意的是常量池的计数是从1开始的而不是编程中习惯的0开始,这是因为在设计之初0这个特殊值被考虑作为一种特殊情况而得以保留,此处不作展开。在常量池的容量计数器之后的项目就是常量池内的具体内容了。常量池内的每一个项目都是一个表,在JDK1.7之前总共有11种表对应不同的类型,在JDK1.7中扩充至14种。这14种表的结构都不相同,但有一个共同点,每个表的第一项都是一个u1类型的tag值,用于表明表的类型。
以下是tag值对应的表类型,取自《深入理解java虚拟机》
由于每一种表的结构都不相同,要理解每一项的内容还需要知道表的结构,以下为表结构汇总表,出处同上
OK,知识储备完成,现在开始具体分析。对应javap的反汇编结果查看以上两张表我们就能获悉常量池的具体内容了。此处我以最简单的Helloworld为例,具体代码以及部分反汇编结果如下
public class Hello{
public static void main(String[] args){
System.out.println("Hello world!");
}
}
D:\Programs\JAVA>javap -verbose Hello.class
Classfile /D:/Programs/JAVA/Hello.class
Last modified 2019-11-11; size 416 bytes
MD5 checksum 2c1d43ed3833eb9bb18d2619f2cc53b2
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Hello.java"
以上是反汇编的结果,但是要想真正了解Class文件的结构我们还需要查看Class源文件,这里我放上了Hello.class源文件的16进制代码,Class文件直接用文本方式打开是无法查看其字节码的,此处使用VSCode的Hexdump插件解析Class文件
前8个字节CA FE BA BE 00 00 00 34分别为Class文件的魔数(image number)CAFABABE和版本号52(34的十进制为52,对应Java 1.8版本),此处不做过多讨论。接下来第08、09偏移位置的值为001D,即为29,查看javap -verbose反汇编出来的代码我们可以发现,常量池有28项,以此我们知道08、09偏移位置的信息就是常量池的容积(上文提及0为特殊情况而保留不参与计数,因此从1开始计算常量池项目数总共有28项)。由于常量池容积为一个u2类型的无符号数,最大可表示范围为65535,如果一个文件中定义超过了这个数量,java文件将无法编译。
标红为常量池容积
常量池容积之后就是常量池内的第一个项目了,Class源文件中偏移位置为0A的列值为0A,对应十进制的10,查询tag值对应类型表我们知道这是一个方法的引用(Methodref_infor),再查询结构汇总表,发现方法引用这个类型的表含有3项,除第一项的tag以外,后两项都是u2类型的索引值,共占4个字节,索引的偏移量为06和0F,就是第6和第15项,分别为类描述和名称及类型描述。
标红为第2项内容
标红处为反汇编后代码的第1项内容,与Class文件中的内容一致
接着我们查看第2项,第2项的tag值为09,查表对应为字段引用(Fieldref_infor),根据结构汇总表,字段引用与第一项方法引用类似也含有3项,包括u1类型的tag和两个u2类型的index,分别为字段的类或接口描述符的索引和字段描述符的索引,两个索引的偏移量分别为10和11,也就是第16和17项。这样我们就知道了常量池中第二项为一个字段引用Fieldref,其中包含了对常量池中第16和17项的引用。
第三个常量的tag为08,查表后得知对应为一个字符串类型的字面量String_info,其包括了一个tag和一个u2类型的字面量的索引index,根据Class文件内容可以看到索引值为12,即对应常量池第18项。我们查看第18项,其tag为1,查表得知其为一个utf-8编码的字符串,内部包含了3各部分,u1类型的tag值,u2类型的字符串长度length,以及length个u1类型的utf-8编码字符。可以看到length值为0c,即字符串长度为12,lengh后的12个字节都是字符串的内容,即为48 65 6C 6C 6F 20 77 6F 72 6C 64 21,转换为字符就是“Hello world!”。也就是说“Hello world!”这个字符串是放在常量池第18项中的,被第三项引用。
第18项从第9行第01个偏移位置始到第0F个偏移位
此处就不再做过多的描述,文中分析了常量池中4项数据的具体内容,并对照Class源文件信息和javap -verbose反汇编的代码来帮助理解常量池内数据的具体存储方式。根据以上的方法,我们就能够获悉Class文件中常量池内容的具体结构和对应数据的含义了。Class文件中的重要内容不仅仅包含常量池,此处只对常量池作了分析,如有不正确的地方还请各位看官不吝赐教!