13. 字节码文件解析

魔数

魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE

版本信息

魔数之后的4个字节为版本信息,前2个字节是次版本号(minor version),接下来2个字节是主版本号(major version)。主版本52对应的是java8(1.8)。minor version是0,major version是52,所以,该文件的版本号为1.8.0。可以通过java -version命令来验证这一点。

常量及常量池

常量池(constant pool):紧接着主版本号之后的就是常量池入口。一个Java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如说Java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。

常量池的总体结构:Java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分构成。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。JVM在解析常量池时,会根据这个u1类型在获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达不引用任何一个常量池的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以,常量池的索引从1而非0开始

class文件结构11中数据类型的结构总表


13. 字节码文件解析_第1张图片
004. class文件结构11中数据类型的结构总表.png

上面的表中描述了11种数据类型的结构,其实在jdk1.7之后又增加了3中(CONSTANT_MethodHandle_info,CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样一种是14种

在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象的全限定名称来标书(/分割而非.)。为了压缩字节码文件的体积,对于基本数据类型,JVM都只用一个大写字母来表示,如下所示:B - byte,C - char,D - double,F - float,I -int,J - long,S - short,Z - boolean,V - void, L - 对象类型(字符串类型:Ljava/lang/String;)

对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[Ljava/lang/String;

用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnameByIdAndNickname(int id, String name)的描述符为:(I, Ljava/lang/String;)Ljava/lang/String;

访问标志

access_flag访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final

标记名 含义
ACC_PUBLIC 0x0001 可以被包的类外访问
ACC_FINAL 0x0010 不允许有子类
ACC_SUPER 0x0020 使用新的invokespecial语义
ACC_INTERFACE 0x0200 标识定义的是接口而不是类
ACC_ABSTRACT 0x0400 不能被实例化
ACC_SYNTHETIC 0x1000 标识并非Java源码生成的代码
ACC_ANNOTATION 0x2000 标识注解类型
ACC_ENUM 0x4000 标识枚举类型

本例子中access_flag是0x0021,是0x0020与0x0001的并集

当前类名、父类名、接口

当前类名,占两个字节,是常量池的索引

父类名,占两个字节,是常量池的索引

接口分两部分组成,接口数量,对应的接口索引,从常量池中找

如果接口数量是0,则没有对应的接口索引

字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量

fields_count:u2 字段的数量

每一个字段的结构

类型 名称 数量
u2 access_flags(访问修饰符) 1
u2 name_index(名字的索引) 1
u2 descriptor_index(描述符的索引) 1
u2 attributes_count(字段拥有的独有的信息) 1
attribute_info attributes(attributes_count为0,则无该类型) attributes_count

上述表格用数据结构表示

field_info{
  u2 access_flag;
  u2 name_index;
  u2 descriptor_index;
  attribute_info attributes[attributes_count];
}

名字+描述符可以唯一确定一个字段,再加上访问修饰符,这个字段的完整信息确确实实就确定下来了

方发表

methods_count:u2

方发表结构

类型 名称 数量
u2 access_flags(访问修饰符) 1
u2 name_index(名字的索引) 1
u2 descriptor_index(描述符的索引) 1
u2 attributes_count(方法拥有的独有的信息) 1
attribute_info attributes(attributes_count为0,则无该类型) attributes_count

上述表格用数据结构表示

method_info{
  u2 access_flag;
  u2 name_index;
  u2 descriptor_index;
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

如果我们没有编写构造方法,Java编译器会自动生成一个无参的构造方法,对成员变量的赋值,非静态代码块,是在构造方法里面,会生成相对应的指令
有多个构造方法,会在每一个方法里面生成对应的指令,如果我们编写了自己的构造方法,那么也会生成对应的指令,然后执行我们自己的代码,如果一个构造方法A调用了另外一个构造方法B,那个A中不会生成对应的指令

方法是自动生成的,对静态信息的初始化(静态变量,静态代码块)
多个静态代码块合并成一个方法

属性结构

属性结构,每个属性都是一个attribute_info结构,用数据结构表示

attribute_info{
  u2 attribute_name_index;
  u4 attribute_length;
  u1 info[attribute_length];
}

JVM预定了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用

不同的attribute通过attribute_name_index来区分

Code结构

Code attribute的作用是保存该方法的结构,用数据结构表示为

Code_attribute {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 max_stack;
  u2 max_locals;
  u4 code_length;
  u1 code[code_length];
  u2 exception_table_length;
  {
    u2 start_pc;
    u2 end_pc;
    u2 handler_pc;
    u2 catch_type;
  } exception_table[exception_table_length];
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段

max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度

max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量

code_length表示该方法所包含的字节码的字节数以及具体的指令码,具体字节码即是该方法被调用时,虚拟机所执行的字节码。如果安装了jclasslib插件,点击对应的指令可以跳到oracle的帮助文档查看具体的指令

exception_table,这里存放的是处理异常的信息

exception_table_length表示异常表的长度

每个exception_table表项由start_pc、end_pc、handler_pc和catch_type组成

start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理

handler_pc表示处理异常的代码的开始处。

catch_type表示会被处理的异常的类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常

Java字节码对于异常的处理方式

  1. 统一采用异常表的方式对异常进行处理
  2. 在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令方式
  3. 当异常处理存在finally语句块时,现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块后面,换句话说,程序中存在多少个catch块,就会
    在每一个catch块后面重复多少个finally语句块的字节码

try catch finally捕获的异常 与 throws抛出的异常 这两种在class文件中的位置是不一样的

LineNumberTable属性

LineNumberTable,这个属性用来表示code数组中的字节码和Java代码行数之间的关系,这个属性可以用来在调试的时候定位代码执行的行数

LineNumberTable_attribute{
  u2 attribute_name_index;
  u4 attribute_length;
  u2 line_number_table_length;
  {
    u2 start_pc;// start_pc是这个line_number_info 描述的字节码指令的偏移量
    u2 line_number;// line_number是这个line_number_info 描述的字节码指令对应的源码中的行号
  }line_number_table[line_number_table_length];
}

LocalVariableTable属性

LocalVariableTable,这个属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系

LocalVariableTable_attribute{
  u2 attribute_name_index;
  u4 attribute_length;
  u2 local_variable_table_length;
  {
    u2 start_pc;// start_pc是当前local_variable_info所对应的局部变量的作用域的起始字节码偏移量;
    u2 length;// length是当前local_variable_info所对应的局部变量的作用域的大小。 也就是从字节码偏移量start_pc 到start_pc+length就是当前局部变量的作用域范围;
    u2 name_index;// name_index指向常量池中的一个CONSTANT_Utf8_info, 该CONSTANT_Utf8_info描述了当前局部变量的变量名
    u2 descriptor_index;// descriptor_index指向常量池中的一个CONSTANT_Utf8_info, 该CONSTANT_Utf8_info描述了当前局部变量的描述符
    u2 index;// index描述了在该方法被执行时,当前局部变量在栈帧中局部变量表中的位置
  }local_variable_table[local_variable_table_length];
}

在Java中,每一个方法里面都是可以访问this的,this表示对当前对象的引用,从字节码角度来说,如果方法是一个非静态方法,this实际上是作为方法的第一个参数给隐式传递过来的,this是编译完成之后,编译器隐式传递过来的,这样,每一个非静态方法都可以顺利访问this。所以对于一个非静态方法,至少会有一个LocalVariableTable,这个变量就是this

对于Java类中的每一个实例方法(非static方法),其实在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个,多的参数是this,他位于方法的第一个参数位置处;这样,我们就可以在Java的实例方法中使用this来去访问当前对象的属性以及其他方法

这个操作时在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数,所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量

SourceFileg属性

SourceFileg,SourceFile属性是类文件结构的attributes表中的一个可选的固定长度属性,类文件结构的attributes表中最多可以有一个SourceFile属性。

SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 sourcefile_index;
}

StackMapTable属性

StackMapTable,是Code attributes表中的一个可变长度属性,在通过类型检查进行验证的过程中使用。一个Code attributes表中最多可以有一个StackMapTable属性。

在版本号为50.0或更高版本的类文件中,如果方法的代码属性没有StackMapTable属性,则它具有隐式堆栈映射属性。此隐式堆栈映射属性等效于StackMapTable属性,其条目数等于零。

StackMapTable_attribute {
    u2              attribute_name_index;
    u4              attribute_length;
    u2              number_of_entries;
    stack_map_frame entries[number_of_entries];
}

你可能感兴趣的:(13. 字节码文件解析)