类文件结构

分析一波Class类文件的结构,此处所使用的二进制文件查看用的classpy。

6.3 Class类文件的结构

6.3.1 魔数与Class文件的版本

​ Class文件是一组以8位(1个字节)为基础单位的二进制流,中间没有分隔符。每个Class文件的头4个字节成为魔数,它唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件(其实好多文件类型都是使用魔数来识别的)。使用魔数而不是扩展名来进行识别主要是考虑安全性,因为文件扩展名是可以随意改动的。img

​ 紧接着魔数的四个字节存储的是Class文件的版本号(如图所示):第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的(图中0X0034转化为十进制为52,说明这个文件可以被JDK1.8或以上的本地虚拟机执行的Class文件),JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本JDK向下兼容以前版本的Class文件,但不能运行以后版本的class文件,即使文件格式并未发生任何变化。

6.3.2 常量池

​ 理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一次出现的表类型数据项目。

​ 常量池中主要存放两大类常量:字面量和符号引用。

​ 字面量:比较接近Java语言层面的常量概念,如文本串、申明为final的常量值等。

​ 符号引用:编译原理层面,包含三类,即类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java在编译的时候不会像C/C++那样有一个“连接”的过程,而是在虚拟机加载Class文件的时候进行动态连接。Class文件不保存方法字段的最终内存信息,所以这些方法、字段的符号引用不经过运行期转换的话无法得到真正的内存入口地址,无法被虚拟机使用。当虚拟机运行时,需从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。

​ 常量池中每一项常量都是一个表,表的开始第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。之所以说常量池是最繁琐的数据,是因为常量类型各自均有自己的结构。

​ 由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型的常量来描述名称,所以该类型的最大长度就是Java中方法、字段名的最大长度。这里u2类型能表达的最大值是65535。所以Java程序中如果定义了超过64KB英文字符的变量和方法名,将无法编译。javap -verbose可以用于输出Class文件字节码内容。

6.3.3 访问标志

​ 常量池结束之后,紧跟着两个字节代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类则是否被声明为final等。一共有16个标记位可以使用。

6.3.4 类索引、父类索引与接口索引集合

​ 类索引(this_class)和父类索引(super_class)都是u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,即父类索引都不为0。接口索引集合描述这个类实现了哪些接口,这些被实现的接口按Implement语句(如果类本身是一个接口,则是extends语句)后的顺序从左到右排列在接口索引集合中。

​ 类索引和父类索引用两个u2类型索引值表示,各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过其中的索引值找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

​ 对于接口索引集合,入口第一项——u2类型的数据为接口计数器(interfaces_count),表示索引容量。如果该类没有实现任何接口,则计数器为0,后面索引表不占任何字节。如图所示。

6.3.5 字段表集合

​ 用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。可以包括的信息有:字段的作用域(public、private、protected)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述信息修饰符都是布尔值,要么有某个修饰符,要么没有。而字段叫什么、字段被定义为什么数据类型,这些都是无法固定的只能引用常量池中的常量来描述。

​ 表中的两项索引值:name_index和descriptor_index,它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

​ 以书中的代码6-1为例,“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅把类全名中的“.”替换成了“/”而已,为了使多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”,表示全限定名结束。简单名称指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是“inc”和“m”。

​ 相对于全限定名和简单名称来说,方法和字段的描述符复杂一些,描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

6.3.6 方法表集合

​ Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。

​ 方法的定义可以通过标志、名称索引、描述符索引表达清楚,方法里的java代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。

​ 与字段表中集合相对应,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。同样有可能出现由编译器自动添加的方法,最典型的就是类构造器”“方法和实例构造器”“方法。

​ 在Java中,重载(Overload)一个方法除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java中无法仅仅依靠返回值的不同来对一个已有方法进行重载的。

0%