class文件(二)
字段表集合:
用于描述接口或类中声明的变量
包括类级变量以及实例级变量,不包括方法内部声明的局部变量
字段的修饰符包括:
作用域:public、private、protected修饰符
实例还是类变量:static
可变性:final
并发可见性:volatile(是否强制从主内存读写)
是否可被序列化:transient
字段数据类型:基本类型、对象、数组
字段名称
字段表结构:
access_flags:字段修饰符,也是通过或的方式将不同的标志位组合在一起形成一个u2类型的数据
name_index和descriptor_index:都是对常量池项的引用,代表字段的简单名称的描述符
先来区分一下全限定名、简单名称、描述符:
全限定名:类全名的.替换成/
简单名称:没有类型和参数修饰的方法或字段名称
描述符:描述字段的数据类型、方法的参数列表和返回值
基本数据类型以及代表无返回值的void类型都用一个大写字符来表示
对于数组类型,每一维度使用一个前置的 [ 来描述,比如int[]被记录为[I,int[][]就是[[I
对于方法,按照先参数列表、后返回值的顺序描述,参数列表之间没有空隙,放在()中
对于例子中,字段表集合开始的第一个u2类型的数据为容量计数器,表示这个类有三个字段表数据
接下来的一个u2数据表示access_flags字段修饰符,0x0002代表private修饰符的标志位为真,其他修饰符为假,
接下来的0x0005表示name_index,可以从常量池中得到第五个常量是一个utf8_info类型的数据,数据内容为m_private,
接下来的0x0006表示descriptor_index,可以从常量池中看到第六个常量也一个utf8_info类型的数据,内容为I,表示这个字段是一个int类型的
综上,可以得到这个字段的信息为private int m_private;
字段表包含的固定数据项目到descriptor_index就结束了,在descriptor_index之后还跟随一个属性表集合,用于存储一些额外的信息。这里的attributes_count是0,表示这里没有额外的属性
剩下两个字段同理分析
字段表不会列出来从父类或父接口继承而来的字段,但是有可能出现原本java代码之中不存在的字段,譬如内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段
java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同都不能重载,但是对于class文件来说,只要两个字段的描述符不是完全相同,那字段重名就是合法的
方法表集合:
class文件中对方法的描述和对字段的描述采用了几乎完全一致的方式,结构如下:
在这个方法表中只是存放了关于方法的访问标志、名称、描述符(参数列表和返回值类型),方法里的代码经过javaac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“code”的属性中
首先一个u2类型的数据表示方法表中有两个方法
首先是第一个方法:0x0000表示访问标志,0x0009表示方法名索引,0x000a表示描述符索引,0x0001表示属性表集合有一项,0x000b表示属性名称的索引值
去常量池中检索可以得到这个方法名是<init>,描述符为()v,表示没有参数没有返回值,属性名对应常量为"Code",说明这个属性是方法代码的字节码描述
如果父类方法在子类中没有被重写,方法集合中就不会出现来自父类的方法信息
方法集合表也可能会出现由编译器自动添加的方法,最常见的是类构造器方法<clinit()>和方法和实例构造器方法<init()>
java中的重载除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,因为返回值不包含在特征签名中,所以java不能仅靠返回值来进行方法重载
但是在class文件格式中,特征签名的范围会更大一些,只要描述符不同的方法就可以共存,也即是说即使有相同的名称和特征签名但是返回值不同就可以合法共存在一个class文件中
属性表集合:
属性表结构:
前面已经分析属性表后面一个紧接着的u2类型的数据表示属性名索引,在常量池中可以检索到是“Code”, 代表这个属性表内的属性是java代码编译成的字节码指令
code属性的结构如下:
首先是属性名索引,指向常量池中的一项utf8_info的数据,属性名固定是code
属性长度,由于属性名和属性长度分别占2字节和4字节,所以属性值的长度固定为整个属性列表长度-6个字节
max_stack:操作数栈深度的最大值
max_locals:局部变量表所需的存储空间,单位是变量槽,变量槽是虚拟机为局部变量分配内存所使用的最小单位
对于长度不超过32位的数据类型,每个局部变量占用一个变量槽
对于double和long这两个64位的数据类型则需要两个变量槽来存放
各种方法参数、方法体中定义的局部变量都需要依赖局部变量表来存放
java虚拟机会对变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用
code_length和code用来存储java源程序编译后生成的字节码指令
当虚拟机读到code中的一个字节码时,就可以对应找出这个字节码代表什么指令,并且可以知道这个指令后面是否还需要跟随参数,以及后续参数如何解析
例如上面的例子中,翻译字节码指令“2a b7 00 01 b1”的过程为:
1.读入2a,查表得到0x2a对应的指令为aload_0,这个指令的含义是将第0个变量槽中为reference类型的本地变量推送到操作数栈顶
2.读入b7.查表得到0xb7对应的指令为invokespecial,
这个指令是将栈顶的reference类型的数据所指向的对象作为方法接收者,
调用此对象的实例构造器方法、private方法或父类的方法
这个方法有一个u2类型的参数说明调用哪个方法,指向常量池中的一个Methodref_info(此方法的符号引用)
3.读入0001,这个是invokespecial指令的参数,代表一个符号引用,查常量池的0x0001对应的常量为实例构造器<init>()的符号引用
4.读入b1,查表得到0xb1对应的指令为return,含义是从方法返回,且返回值为void,这条指令执行后,当前方法正常结束