类文件结构的故事(二)


读常量池的方法

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

如今举个例子,为你我加深下印象。web

CONSTANT_Class_info 这张表举例子。它的结构以下:数组

类型 名称 数量
u1 tag 1
u2 name_index 1

第一个数据项是 tag 是标记位的意思,第二个数据项 name_index 是类或者接口的全限定名。它的值是个 u2 类型,换算成十进制,指的就是常量池中的第几个表。从 1 开始 计算, 0 以前已经说过,被系统设置了。jvm


UTF-8编码的字符串表

这个比较特别,常量池中全部的引用,最后都指向一张字符串表,可是不是指向同一张。值得单独说下。svg

CONSTANT_Utf8_info 的结构以下:工具

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

第一个仍是标记位;第二个是长度,表示从当前开始,日后的 length 个字节就是该表 表示的数据。编码


访问标记

常量池结束之后,紧跟着的是 访问标记 ,占用两个字节,用来描述接口或者类的信息,好比,是不是 abstract ,是否被定义 public 仍是 private ,若是是类,是否被修饰为 final3d

一样的书上给出可一个关系表,列出了对应的关系,好比 ACC_PUBLIC 对应的标志值为 0x0001 ,表示是否为 publicACC_FINAL 对应的标志值为 0x0010 ,表示是不是 final 类;ACC_SUPER 对应的标志值为 0x0020JDK1.2 之后编译出来的类,这个标志就为真;其余的就不一一列举了,上面推荐的工具,直接显示了该类的类型;code

在这里插入图片描述
主要说下这个标志值是怎么计算的,首先有许多标志位,只有被用的标志位才会被设置为对应的标志值,没用到的,通通设置为 0xml

好比这里使用到了 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 属性 。

相关文章
相关标签/搜索