常量池中各数据项类型详解(续)
(8) constant_class_info
常量池中的一个constant_class_info, 可以看做是constant_class数据类型的一个实例。 他是对类或者接口的符号引用。 它描述的可以是当前类型的信息, 也可以描述对当前类的引用, 还可以描述对其他类的引用。 也就是说, 如果访问了一个类字段, 或者调用了一个类的方法, 对这些字段或方法的符号引用, 必须包含它们所在的类型的信息, constant_class_info就是对字段或方法符号引用中类型信息的描述。
constant_class_info的第一个字节是tag, 值为7, 也就是说, 当虚拟机访问到一个常量池中的数据项, 如果发现它的tag值为7, 就可以判断这是一个constant_class_info 。 tag下面的两个字节是一个叫做name_index的索引值, 它指向一个constant_utf8_info, 这个constant_utf8_info中存储了constant_class_info要描述的类型的全限定名。
此外要说明的是, java中数组变量也是对象, 那么数组也就有相应的类型, 并且数组的类型也是使用constant_class_info描述的, 并且数组类型和普通类型的描述有些区别。 普通类型的constant_class_info中存储的是全限定名, 而数组类型对应的constant_class_info中存储的是数组类型相对应的描述符字符串。 举例说明:
与object类型对应的constant_class_info中存储的是: java/lang/object
与object[]类型对应的constant_class_info中存储的是: [ljava/lang/object;
下面看constant_class_info的存储布局:
例如, 如果在一个类中引用了system这个类, 那么就会在这个类的常量池中出现以下信息:
(9) constant_fieldref_info
常量池中的一个constant_fieldref_info, 可以看做是constant_field数据类型的一个实例。 该数据项表示对一个字段的符号引用, 可以是对本类中的字段的符号引用, 也可以是对其他类中的字段的符号引用, 可以是对成员变量字段的符号引用, 也可以是对静态变量的符号引用, 其中ref三个字母就是reference的简写。 之前的文章中, “符号引用”这个名词出现了很多次, 可能有的同学一直不是很明白, 等介绍完constant_fieldref_info, 就可以很清晰的了解什么是符号引用。 下面分析constant_fieldref_info中的内容都存放了什么信息。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为9 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为9, 就可以确定这个被访问的数据项是一个constant_fieldref_info, 并且知道这个数据项表示对一个字段的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个constant_class_info数据项, 这个数据项表示被引用的字段所在的类型, 包括接口。 所以说, constant_class_info可以作为字段符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个constant_nameandtype_info 。 这个constant_nameandtype_info描述的是被引用的字段的名称和描述符。 我们在前面的博客中也提到过, constant_nameandtype_info可以作为字段符号引用的一部分。
到此, 我们可以说, constant_fieldref_info就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该字段所在的类, 另一部分是该字段的字段名和描述符。 这就是所谓的 “对字段的符号引用” 。
下面结合实际代码来说明, 代码如下:
1
2
3
4
5
6
7
|
package com.bjpowernode.test; public class testint { int a = 10 ; void print(){ system.out.println(a); } } |
在print方法中, 引用了本类中的字段a。 代码很简单, 我们一眼就可以看到print方法中是如何引用本类中定义的字段a的。 那么在class文件中, 对字段a的引用是如何描述的呢? 下面我们将这段代码使用javap反编译, 给出简化后的反编译结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
constant pool: # 1 = class # 2 // com/bjpowernode/test/testint # 2 = utf8 com/bjpowernode/test/testint ...... # 5 = utf8 a # 6 = utf8 i ...... # 12 = fieldref # 1 .# 13 // com/bjpowernode/test/testint.a:i # 13 = nameandtype # 5 :# 6 // a:i ...... { void print(); flags: code: stack= 2 , locals= 1 , args_size= 1 0 : getstatic # 19 // field java/lang/system.out:ljava/io/printstream; 3 : aload_0 4 : getfield # 12 // field a:i 7 : invokevirtual # 25 // method java/io/printstream.println:(i)v 10 : return } |
可以看到, print方法的位置为4的字节码指令getfield引用了索引为12的常量池数据项, 常量池中索引为12的数据项是一个constant_fieldref_info, 这个constant_fieldref_info又引用了索引为1和13的两个数据项, 索引为1的数据项是一个constant_class_info, 这个constant_class_info数据项又引用了索引为2的数据项, 索引为2的数据项是一个constant_utf8_info , 他存储了字段a所在的类的全限定名com/bjpowernode/test/testint 。 而constant_fieldref_info所引用的索引为13的数据项是一个constant_nameandtype_info, 它又引用了两个数据项, 分别为第5项和第6项, 这是两个constant_utf8_info , 分别存储了字段a的字段名a, 和字段a的描述符i 。
下面给出内存布局图, 这个图中涉及的东西有点多, 因为constant_fieldref_info引用了constant_class_info和constant_nameandtype_info, constant_class_info又引用了一个constant_utf8_info , 而constant_nameandtype_info又引用了两个constant_utf8_info 。
(10) constant_methodref_info
常量池中的一个constant_methodref_info, 可以看做是constant_methodref数据类型的一个实例。 该数据项表示对一个类中方法的符号引用, 可以是对本类中的方法的符号引用, 也可以是对其他类中的方法的符号引用, 可以是对成员方法字段的符号引用, 也可以是对静态方法的符号引用,但是不会是对接口中的方法的符号引用。 其中ref三个字母就是reference的简写。 在上一小节中介绍了constant_fieldref_info, 它是对字段的符号引用, 本节中介绍的constant_methodref_info和constant_fieldref_info很相似。既然是符号“引用”, 那么只有在原文件中调用了一个方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个constant_methodref_info。 如果只是在类中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的constant_methodref_info 。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为10 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为10, 就可以确定这个被访问的数据项是一个constant_methodref_info, 并且知道这个数据项表示对一个方法的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个constant_class_info数据项, 这个数据项表示被引用的方法所在的类型。 所以说, constant_class_info可以作为方法符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个constant_nameandtype_info 。 这个constant_nameandtype_info描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, constant_nameandtype_info可以作为方法符号引用的一部分。
到此, 我们可以知道, constant_methodref_info就是对一个字段的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的类, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对方法的符号引用” 。
下面结合实际代码来说明, 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.bjpowernode.test; public class programer { computer computer; public programer(computer computer){ this .computer = computer; } public void dowork(){ computer.calculate(); } } package com.bjpowernode.test; public class computer { public void calculate() { system.out.println( "working..." ); } } |
上面的代码包括两个类, 其中programer类引用了computer类, 在programer类的dowork方法中引用(调用)了computer类的calculate方法。源码中对一个方法的描述形式我们再熟悉不过了, 现在我们就反编译programer, 看看programer中对computer的dowork方法的引用, 在class文件中是如何描述的。
下面给出programer的反编译结果, 其中省去了一些不相关的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
constant pool: ..... # 12 = utf8 ()v # 20 = methodref # 21 .# 23 // com/bjpowernode/test/computer.calculate:()v # 21 = class # 22 // com/bjpowernode/test/computer # 22 = utf8 com/bjpowernode/test/computer # 23 = nameandtype # 24 :# 12 // calculate:()v # 24 = utf8 calculate { com.bjpowernode.test.computer computer; flags: ...... public void dowork(); flags: acc_public code: stack= 1 , locals= 1 , args_size= 1 0 : aload_0 1 : getfield # 13 // field computer:lcom/bjpowernode/test/computer; 4 : invokevirtual # 20 // method com/bjpowernode/test/computer.calculate:()v 7 : return } |
可以看到, dowork方法的位置为4的字节码指令invokevirtual引用了索引为20的常量池数据项, 常量池中索引为20的数据项是一个constant_methodref_info, 这个constant_methodref_info又引用了索引为21和23的两个数据项, 索引为21的数据项是一个constant_class_info, 这个constant_class_info数据项又引用了索引为22的数据项, 索引为22的数据项是一个constant_utf8_info , 他存储了被引用的computer类中的calculate方法所在的类的全限定名com/bjpowernode/test/computer 。 而constant_methodref_info所引用的索引为23的数据项是一个constant_nameandtype_info, 它又引用了两个数据项, 分别为第24项和第12项, 这是两个constant_utf8_info , 分别存储了被引用的方法calculate的方法名calculate, 和该方法的描述符()v 。
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为constant_methodref_info引用了constant_class_info和constant_nameandtype_info, constant_class_info又引用了一个constant_utf8_info , 而constant_nameandtype_info又引用了两个constant_utf8_info 。
(11) constant_interfacemethodref_info
常量池中的一个constant_interfacemethodref_info, 可以看做是constant_interfacemethodref数据类型的一个实例。 该数据项表示对一个接口方法的符号引用, 不能是对类中的方法的符号引用。 其中ref三个字母就是reference的简写。 在上一小节中介绍了constant_methodref_info, 它是对类中的方法的符号引用, 本节中介绍的constant_interfacemethodref和constant_methodref_info很相似。既然是符号“引用”, 那么只有在原文件中调用了一个接口中的方法, 常量池中才有和这个被调用方法的相对应的符号引用, 即存在一个constant_interfacemethodref。 如果只是在接口中定义了一个方法, 但是没调用它, 则不会在常量池中出现和这个方法对应的constant_interfacemethodref 。
和其他类型的常量池数据项一样, 它的第一个字节也必然是tag, 它的tag值为11 。 也就是说, 当虚拟机访问到一个常量池中的一项数据, 如果发现这个数据的tag值为11, 就可以确定这个被访问的数据项是一个constant_interfacemethodref, 并且知道这个数据项表示对一个接口中的方法的符号引用。
tag值下面的两个字节是一个叫做class_index的索引值, 它指向一个constant_class_info数据项, 这个数据项表示被引用的方法所在的接口。 所以说, constant_class_info可以作为方法符号引用的一部分。
class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个constant_nameandtype_info, 这个constant_nameandtype_info描述的是被引用的方法的名称和描述符。 我们在前面的博客中也提到过, constant_nameandtype_info可以作为方法符号引用的一部分。
到此, 我们可以知道, constant_interfacemethodref就是对一个接口中的方法的符号引用, 这个符号引用包括两部分, 一部分是该方法所在的接口, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对接口中的方法的符号引用” 。
下面结合实际代码来说明, 代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
package com.bjpowernode.test; public class plane { iflyable flyable; void flytosky(){ flyable.fly(); } } package com.bjpowernode.test; public interface iflyable { void fly(); } |
在上面的代码中, 定义可一个类plane, 在这个类中有一个iflyable接口类型的字段flyable, 然后在plane的flytosky方法中调用了iflyable中的fly方法。 这就是源代码中对一个接口中的方法的引用方式, 下面我们反编译plane, 看看在class文件层面, 对一个接口中的方法的引用是如何描述的。
下面给出反编译结果, 为了简洁期间, 省略了一些不相关的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
constant pool: ...... # 8 = utf8 ()v # 19 = interfacemethodref # 20 .# 22 // com/bjpowernode/test/iflyable.fly:()v # 20 = class # 21 // com/bjpowernode/test/iflyable # 21 = utf8 com/bjpowernode/test/iflyable # 22 = nameandtype # 23 :# 8 // fly:()v # 23 = utf8 fly { ....... com.bjpowernode.test.iflyable flyable; flags: ....... void flytosky(); flags: code: stack= 1 , locals= 1 , args_size= 1 0 : aload_0 1 : getfield # 17 // field flyable:lcom/bjpowernode/test/iflyable; 4 : invokeinterface # 19 , 1 // interfacemethod com/bjpowernode/test/iflyable.fly:()v 9 : return } |
可以看到, flytosky方法的位置为4的字节码指令invokeinterface引用了索引为19的常量池数据项, 常量池中索引为19的数据项是一个constant_interfacemethodref_info, 这个constant_interfacemethodref_info又引用了索引为20和22的两个数据项, 索引为20的数据项是一个constant_class_info, 这个constant_class_info数据项又引用了索引为21的数据项, 索引为21的数据项是一个constant_utf8_info , 他存储了被引用的方法fly所在的接口的全限定名com/bjpowernode/test/iflyable 。 而constant_interfacemethodref_info所引用的索引为22的数据项是一个constant_nameandtype_info, 它又引用了两个数据项, 分别为第23项和第8项, 这是两个constant_utf8_info , 分别存储了被引用的方法fly的方法名fly, 和该方法的描述符()v 。
下面给出内存布局图, 这个图中涉及的东西同样有点多, 因为constant_interfacemethodref_info引用了constant_class_info和constant_nameandtype_info, constant_class_info又引用了一个constant_utf8_info , 而constant_nameandtype_info又引用了两个constant_utf8_info 。
总结
到此为止, class文件中的常量池部分就已经讲解完了。 进行一下总结。对于深入理解java和jvm , 理解class文件的格式至关重要, 而在class文件中, 常量池是一项非常重要的信息。 常量池中有11种数据项, 这个11种数据项存储了各种信息, 包括常量字符串, 类的信息, 方法的符号引用, 字段的符号引用等等。 常量池中的数据项通过索引来访问, 访问形式类似于数组。 常量池中的各个数据项之前会通过索引相互引用, class文件的其他地方也会引用常量池中的数据项 , 如方法的字节码指令。
在下面的文章中, 会继续介绍class文件中, 位于常量池以下的其他信息。 这些信息包括:对本类的描述, 对父类的描述, 对实现的接口的描述, 本类中声明的字段的描述, 本类汇总定义的方法的描述,还有各种属性。