类文件结构的故事(二)


读常量池的方法

之前简单的说了下,如何读常量池里面的数据项。

现在举个例子,为你我加深下印象。

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 ,表示是否为 publicACC_FINAL 对应的标志值为 0x0010 ,表示是否是 final 类;ACC_SUPER 对应的标志值为 0x0020JDK1.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_indexdescriptor_index ,都是索引,指向常量池;

  1. name_index :简单名称,int number(); 的简单名称就是 number

  2. 类的 全限定名 ,将 类全名 中的 . 换成 /

  3. 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 ;

上面的例子只是描述字段,描述方法则按照以下顺序:

  1. 参数列表;参数列表按照参数定义的顺序,放在一个 () 里面;

  2. 返回值 ;

    比如 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 属性 。

posted @ 2019-08-13 18:57  Yiaz  阅读(138)  评论(0编辑  收藏  举报