以前简单的说了下,如何读常量池里面的数据项。java
如今举个例子,为你我加深下印象。web
拿 CONSTANT_Class_info
这张表举例子。它的结构以下:数组
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
第一个数据项是 tag
是标记位的意思,第二个数据项 name_index
是类或者接口的全限定名。它的值是个 u2
类型,换算成十进制,指的就是常量池中的第几个表。从 1
开始 计算, 0
以前已经说过,被系统设置了。jvm
这个比较特别,常量池中全部的引用,最后都指向一张字符串表,可是不是指向同一张。值得单独说下。svg
CONSTANT_Utf8_info
的结构以下:工具
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
第一个仍是标记位;第二个是长度,表示从当前开始,日后的 length
个字节就是该表 表示的数据。编码
常量池结束之后,紧跟着的是 访问标记 ,占用两个字节,用来描述接口或者类的信息,好比,是不是 abstract
,是否被定义 public
仍是 private
,若是是类,是否被修饰为 final
;3d
一样的书上给出可一个关系表,列出了对应的关系,好比 ACC_PUBLIC
对应的标志值为 0x0001
,表示是否为 public
;ACC_FINAL
对应的标志值为 0x0010
,表示是不是 final
类;ACC_SUPER
对应的标志值为 0x0020
, JDK1.2
之后编译出来的类,这个标志就为真;其余的就不一一列举了,上面推荐的工具,直接显示了该类的类型;code
主要说下这个标志值是怎么计算的,首先有许多标志位,只有被用的标志位才会被设置为对应的标志值,没用到的,通通设置为 0
;xml
好比这里使用到了 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
属性 。