类文件结构的故事(二)
读常量池的方法
之前简单的说了下,如何读常量池里面的数据项。
现在举个例子,为你我加深下印象。
拿 CONSTANT_Class_info
这张表举例子。它的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
第一个数据项是 tag
是标记位的意思,第二个数据项 name_index
是类或者接口的全限定名。它的值是个 u2
类型,换算成十进制,指的就是常量池中的第几个表。从 1
开始 计算, 0
之前已经说过,被系统设置了。
UTF-8编码的字符串表
这个比较特别,常量池中所有的引用,最后都指向一张字符串表,但是不是指向同一张。值得单独说下。
CONSTANT_Utf8_info
的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
第一个还是标记位;第二个是长度,表示从当前开始,往后的 length
个字节就是该表 表示的数据。
访问标记
常量池结束以后,紧跟着的是 访问标记 ,占用两个字节,用来描述接口或者类的信息,比如,是否是 abstract
,是否被定义 public
还是 private
,如果是类,是否被修饰为 final
;
同样的书上给出可一个关系表,列出了对应的关系,比如 ACC_PUBLIC
对应的标志值为 0x0001
,表示是否为 public
;ACC_FINAL
对应的标志值为 0x0010
,表示是否是 final
类;ACC_SUPER
对应的标志值为 0x0020
, JDK1.2
以后编译出来的类,这个标志就为真;其他的就不一一列举了,上面推荐的工具,直接显示了该类的类型;
主要说下这个标志值是怎么计算的,首先有许多标志位,只有被用的标志位才会被设置为对应的标志值,没用到的,统统设置为 0
;
比如这里使用到了 final,super,public
,因此,三个标志的标志值进行 |
运算:0x0001 | 0x0020 | 0x0010 = 0x0031
也就是图中工具直接算出来的 0x0031
;
类索引、父类索引、接口索引集合
类索引、父类索引,都是一个 u2
类型的数据项,接口索引集合,从名字上就可以看出是个集合,是一组 u2
类型数据的集合;
Class
文件,通过这三个数据项,进行确定继承关系;
类索引,用于确定该类、接口的全限定名;
父类索引,用于确定该类、接口的父类的全限定名,除了 java.lang.Object
,所有的 Java
类的父类索引都不可以为 0
;
接口索引集合,用于描述该类实现了哪些接口|接口继承了哪些接口,这些接口按照 implement、extend
后面从左到右的顺序,排列到接口索引集合中;
类索引、父类索引,都是一个 u2
类型的数据项,都指向一个 CONSTANT_Class_info
,然后 CONSTANT_Class_info
的索引指向一个 CONSTANT_Utf8_info
,里面保存着对应的 全限定名 ;
接口索引集合,由于是一个集合,所有跟常量池一样,首先有个入口,是一个 u2
类型的接口数量计数器,然后后面才是真正的集合;如果没有实现或继承任何接口,则计算器的值为 0
,后面接口的索引表将不再占用任何字节,也就是不存在;
都是保存着索引的,拿着索引去常量池里面寻找对应的数据;
字段表集合
字段表用于描述接口或类中申明的变量;
这个变量是类中,不管是不是 static
修饰的,也就是类级别的、实例级别的 ,都算,但是方法内部的不算;
保存的信息,变量的修饰符、名字 ;其中修饰符,有数据类型修饰符(int、douple...
),作用域修饰符(private、public...
)、static、final、transient、volatile… ;
上面那些修饰符,除了数据类型修饰符,其他的修饰符,都是要么几选一,要么有,要么没有,都是布尔类型,很适合用一个标记位来表示;至于其他修饰符、类型是千奇百怪的,反正无法想到怎么用一个固定字节来表示,那我们就用字符串记录下,字符串放进常量池里面,这里仅保存引用 ;
字段表的入口,是一个 u2
字节的计数器;
字段表结构如下:
名称 | 类型 | 数量 |
---|---|---|
access_flags |
u2 |
1 |
name_index |
u2 |
1 |
descriptor_index |
u2 |
1 |
attributes_count |
u2 |
1 |
attributes |
attributes_info |
attributes_count |
除了数据类型的字段的修饰符放到 access_flags
和 访问标记 基本一样;
access_flags
之后,就是 name_index
、descriptor_index
,都是索引,指向常量池;
-
name_index
:简单名称,int number();
的简单名称就是number
; -
类的 全限定名 ,将 类全名 中的
.
换成/
; -
descriptor_index
:描述符,字段的数据类型,方法的参数列表(顺序不可乱)和返回值,这些就是字段与方法的描述符;- 基本数据类型和
void
都用一个大写字母表示,对象类型使用L
加上对象的全限定名表示
- 基本数据类型和
标识字符 | 被标识的类型 |
---|---|
B | byte |
C | char |
D | double |
f | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型 |
数组,什么类型的数组以及几维数组,就在对应的类型的标识字符前面添加对应的 [
,比如 int[][] number
则记做 [[I
;
上面的例子只是描述字段,描述方法则按照以下顺序:
-
参数列表;参数列表按照参数定义的顺序,放在一个
()
里面; -
返回值 ;
比如
public String toString(char c,int[] number) ;
的描述符为(C[I)Ljava/lang/String
;
这之后就是额外信息描述计数器和额外信息描述,后面 属性表 那里再讲;字段的值就是放在那里面,我们这里说的都是字段的描述:修饰符、名字等等
字段表集合不会列出父类的字段,但是可能列出一些不存在的字段,比如 内部类的属性中可能会出现外部类引用 ,内部类可以直接访问外部类,那么外部类的可供内部类访问的字段,会被直接添加到内部类的字节码的字段表集合中的;
方法表集合
基本上和 字段表集合 一样;
就不再重复讲了;
主要说下,方法的修饰符、名字、描述符,都被保存起来了;那么方法体呢,被保存在哪里呢?方法体经过编译以后,变为字节码指令,存放在 属性表 集合中的 Code
属性里面 ;
同样父类的方法,如果没有被子类重写,是不会被放到该集合中的;
但是集合中可能出现一些编译器自己添加进去的方法,比如类构造器方法 <clinit>
和实例构造器方法 <init>
;
插播: 为什么重载,不能根据方法返回值进行区别
java
虚拟机规范里面,指定 方法特征签名,仅仅包括了 方法名称、参数顺序以及类型 ;
各大厂商实现的虚拟机,都按照这个规范来的,在寻找方法的时候,仅仅按照方法特征签名来寻找的,而方法特征签名里面,没有返回值;因此,不能通过方法返回值进行区分重载的方法;
而在字节码文件里面,只要描述不是完全一致的方法,是可以共存的,也就是可以通过方法返回值进行区分,只是 jvm规范
中没有将方法返回值算进去,其实技术层面是可以通过返回值进行区分的 ;
因此,虚拟机平台上的其他语法在实现的时候,可以将这个纳入进去,转成字节码的时候 JVM
是认识的,但是 java
里面是无法做到了。
属性表集合
属性表,和前面说的那些不太一样,它在好多地方都有出现,不是一个单独作为一项出现的,也就是它可以出现多次,不似之前的那些数据项只能在规定的地方出现一次。
前面讲的那些,某些数据项中存在属性表属性,其中有: Class文件
、字段表
、方法表
,这三个数据项都有自己的属性表,用于描述一些信息。
属性表本身也是一个表结构,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
属性表的限制不像其他表和双规似的限制那么严格,它对其中的属性没有顺序要求,甚至对属性的个数也没有要求,任何人都可以往属性表里面添加属性,就是你添加的属性,JVM
不认识,在运行的时候会被自动忽略掉而已。
为了能正确的解析 Class
文件,解析里面的属性表的属性,JVM
规范目前制定了 21
个属性,用于描述特定场景的信息。 其中每个属性本身也是一张表。
其中有些属性,只能用在 方法表或者字段表或者类文件中,也有可以三个通用的属性,也有可以用在其他属性的属性。一般是一些属性可以用在 Code
属性 。