第三章 类文件结构

时间:2021-09-08
本文章向大家介绍第三章 类文件结构,主要包括第三章 类文件结构使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

  代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

  Java 虚拟机使用字节码实现了跨平台的愿景,不仅针对Java语言,实现了write once,run anywhere的愿景;随着发展,越来越多其他语言可以在Java虚拟机之上运行,如Kotlin、Clojure、Groovy、JRbuy、JPython、Scala等。

  Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

  

  1. Class类文件的结构

  Class文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在文件之中,中间没有任何分隔符。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1字节、2字节、4字节和8字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
  • 表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表习惯性以“_info”结尾。表用于描述有层次关系的复合结构数据,整个Class文件本质上可以视作一张表,这张表严格按照下表1-1所列顺序排列。

                               表1-1 Class 文件格式

  

  为了方便讲解,准备了如下的Java代码。  

package org.fenixsoft.clazz;
public class TestClass{
    private int m;
    public int inc(){
        return m + 1;
    }
}

  相应的类文件结构如下图所示:

  

  1.1 魔数与Class文件版本

  每个Class文件的头四个字节被称为魔数,用以确定这个文件是否为一个能被虚拟机接受的Class文件,值为0x CA FE BA BE。第五六字节 0x 00 00代表次版本号。主版本号值为0x 00 32,即十进制的50,说明是JDK6或以上版本虚拟机执行的Class文件。

    

   1.2 常量池

  由表1-1可知,主次版本号后,是常量池入口,常量池被比喻为Class文件里面的资源仓库,是Class文件结构中与其他项目关联最多的数据。

  首先是u2类型的数据代表常量池容量计数值(constant_pool_count),这个容量技术从1开始,如果不引用任何常量池项目,则把索引值设置为0。由下图0x 00 16,即十进制的22,代表常量池中有21项常量,索引范围为1~21。

  

  常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),字面量比较接近于Java语言层面的常量概念,如文本字符串,被声明为final常量值等。而符号引用则属于编译原理方面的概念,主要包括以下几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

  常量池中的每一项常量都是一个表,常量表中有17种不同类型的常量,所有表结构起始的第一位是个u1类型的标志位,代表当前常量属于哪种常量类型。

  

  图6-3常量池结构中,常量池容量0x 00 16后紧跟的u1标志位0x 07查表6-3可知,是一个CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的符号引用。

  

  name_index是常量池的索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类的全限定名。本例中的name_index值为0x 00 02,指向了常量池中的第二项常量。第二项常量的标志位是0x 01,查表6-3可知,是CONSTANT_Uff8_info类型的常量。其结构表如表6-5所示。

  

  length值说明了UTF-8字符串长度是多少个字节,后面紧跟长度为length字节的,UTF-8缩略编码表示的字符串。

  UTF-8缩略编码与普通UTF-8编码的区别是:从'\u0001''\u007f'之间的字符(相当于1127ASCII码) 的缩略编码使用一个字节表示,从'\u0080''\u07ff'之间的所有字符的缩略编码用两个字节表示, 从'\u0800'开始到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

  本例中length值长度为0x 00 1D, 长度是29个字节,往后的29个字节都在1~127的ASCII码范围以内,内容为“org/fenixsoft/clazz/TestClass

   

  剩下的19个常量与上面讨论的类似,可以通过javap -verbose 类文件名查看字节码内容,如下图所示。

  

  不难发现,常量池中存在“I”“V”“<init>”“LineNumberTable”“LocalVariableTable”等, 这些看起来在源代码中不存在的常量,它们是编译器自动生成的,会被字段表(field_info)、方法表(method_info)、属性表(attribute_info)所引用,用于表示方法的返回值,有几个参数,每个参数的类型等信息。

  以下是常量池中的17种数据类型的结构总表

  

  

   

  1.3 访问标志

  由表6-1,常量池结束后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。具体标志值及其含义见下表。

  

  没有用到的标志位一律为0。由标志位为0x 00 21可知,ACC_PUBLIC 和 ACC_SUPER标志为真。

   

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

  由表6-1可知,访问标志后是类索引、父类索引与接口索引集合,Class文件由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合用来描述这个类实现了哪些接口,这些实现的接口将按implements关键字后的接口顺序从左到右排列在接口索引集合中。

  类索引(0x 00 01)和父类索引(0x 00 03)用两个u2类型的索引值表示,各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

    

  接口索引为0x 00 00,接口索引集合大小为0.

  1.5 字段表集合

  字段表(field_info) 用于描述接口或者类中声明的变量。 Java语言中的字段Field) 包括类级变量以及实例级变量, 但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域(publicprivateprotected修饰符) 、是实例变量还是类变量(static修饰符) 、 可变性(final) 、 并发可见性(volatile修饰符,是否强制从主内存读写) 、 可否被序列化(transient修饰符) 、 字段数据类型(基本类型、 对象、 数组) 、字段名称。 表6-8为字段表结构。

  

  字段修饰符放在access_flags项目中,其标志位和含义如表6-9所示。

   

  name_index和descriptor_index都是对常量池项的引用,分别代表字段的简单名称以及字段和方法的描述符。

  简单名称指没有类型和参数修饰的方法或者字段名称,最开始的Java代码中,inc()方法和m字段的简单名称是“inc”和“m”。

  描述符用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。描述符标识字符含义见下表。

   

  对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;", 一个整型数组“int[]”将被记录成“[I”

  对于方法,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;", 方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII)I”

   由表6-1,接下来是fields_count,其值为0x 00 01,即这个类只有一个字段表数据。接下来的fields参考表6-8.首先是access_flags标志,值为0x 00 02,查表6-9,为ACC_PRIVATE。name_index的值为0x 00 05,由1.2节所列常量代码清单可知,值为“m”。字段描述符descriptor_index的值为0x 00 06,指向常量池的字符串“I”。可以推断出,原代码定义的字段为“private int m”。紧跟的0x 00 00 表示没有额外属性。 attribute_info的内容将在1.7节进行讲解。

   

  1.6 方法表集合

  Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一样的方式。表6-11为方法表结构,表6-12为方法访问标志。

  

   

  方法里的代码,存放在方法属性表集合中的code属性里面,将在下一节介绍。

  

  0x 00 02 表示有两个方法。第一个方法access_flags为0x 00 01,查表6-12是ACC_PUBLIC,name_index是0x 00 07查常量池表可知方法为“<init>”,接下来的descriptor_index是0x 00 08,对应常量为“()V”,属性表计数器attributes_count的值为0x 00 01,表示此方法的属性表集合有1项属性,属性名称索引值为0x 00 09,对应常量为“code”。

  1.7 属性表集合

  Class文件、字段表、方发表都可以携带自己的属性表集合,以描述某些场景专有的信息。预定义属性有29项,如下表所示,后面章节将会对常用的,重要属性进行解释。

  

  

  1.7.1 Code属性

  Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如表6-15所示。 

    

  attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称。

  attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。

  max_stack代表了操作数栈(Operand Stack) 深度的最大值。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

  max_locals代表了局部变量表所需的存储空间。max_locals的单位是变量槽(Slot) ,变量槽是虚拟机为局部变量分配内存所使用的最小单位。 对于bytecharfloatintshortbooleanreturnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽, 而doublelong这两种64位的数据类型则需要两个变量槽来存放。 Javac编译器会根据变量的作用域来分配变量槽给各个变量使用, 根据同时生存的最大局部变量数量和类型计算出max_locals的大小。

  Javac编译器会根据同时生存的最大局部变量数量和类型计算出max_locals的大小。  

  code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。编码与指令的对应关系可以查看“虚拟机字节码指令表”。

   

  继续上一节分析过的实例构造器“<init()>”方法的code属性。attribute_length为0x 00 00 00 2F。操作数栈的最大深度和本地变量表的容量都为0x 00 01,字节码区域所占空间的长度为0x 00 05。根据字节码指令表翻译出所对应的字节码指令2A B7 000A B1:

  1) 读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个变量槽中为reference类型的本地变量推送到操作数栈顶。

  2) 读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的符号引用。
  3) 读入000A,这是invokespecial指令的参数,代表一个符号引用,查常量池得0x 00 0A对应的常量为实例构造器“<init>()”方法的符号引用。
  4) 读入B1,查表得0xB1对应的指令为return,含义是从方法的返回,并且返回值为void。这条指令执行后,当前方法正常结束。

  使用javap命令把另一个方法的字节码指令也计算出来。  

public int inc();
 Code:
  Stack=2, Locals=1, Args_size=1
  0: aload_01: getfield #18; //Field m:I
  4: iconst_1
  5: iadd
  6: ireturn
LineNumberTable:
  line 8: 0
LocalVariableTable:
  Start Length Slot Name Signature
  0 7 0 this Lorg/fenixsoft/clazz/TestClass;

  异常表格式如表6-16所示。

    

  当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引) , 则转到第handler_pc行继续处理。 当catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理。

  1.7.2 Exceptions属性

  Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常, 每一种受查异常使用一个exception_index_table项表, exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引, 代表了该受查异常的类型。

   

  1.7.3 LineNumberTable属性

  LineNumberTable属性用于描述Java源码行号与字节码行号( 字节码的偏移量) 之间的对应关系。  

    

  line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

  1.7.4 LocalVariableTable属性

  LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。 

    

    

  start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

  name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。

  index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时( double和long),它占用的Slot为index和index+1两个。

   在JDK 1.5引入泛型之后,LocalVariableTable属性增加了一个“ 姐妹属性” :LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名( Signature) ,对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable。

  1.7.5 SourceFile属性

  SourceFile属性用于记录生成这个Class文件的源码文件名称。对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况( 如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。
  

  sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

  1.7.6 ConstantValue属性

  ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量( 类变量) 才可以使用这项属性。类似“ int x=123” 和“ static int x=123” 这样的变量定义在Java程序中是非常常见的事情, 但虚拟机对这两种变量赋值的方式和时刻都有所不同。 对于非static类型的变量( 也就是实例变量) 的赋值是在实例构造器<init>方法中进行的;如果同时使用final和static来修饰一个变量( 按照习惯,这里称“ 常量” 更贴切) ,并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

   

  constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

  1.7.7 InnerClasses属性

  InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。该属性的结构见表6-23。

    inner_classes_info表的结构见表6-24。

   

  inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0。

   inner_class_access_flags是内部类的访问标志,类似于类的access_flags,它的取值范围见表6-25。

   

  1.7.8 Signature属性

  任何类、接口、初始化方法或成员的泛型签名包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会记录泛型签名信息,这样Java 的反射API能够获取泛型类型。

     

原文地址:https://www.cnblogs.com/muxianbai/p/15236623.html