Java虚拟机--Class文件结构(十七)

知识点的梳理:

  • 通过class文件,可以让更多的计算机语言支持Java虚拟机平台。Class文件结构不仅仅是Java虚拟机的执行入口,更是Java生态圈的基础和核心 
  • Class文件
    • Java语言跨平台依靠的是Class文件,它是异构语言和Java虚拟机之间的重要桥梁,下图显示了各种语言由源代码被编译成Class文件,并最终得以在Java虚拟机上执行的过程:
    • 总体结构图:

    • Class文件结构的基本数据类型:
      • Class文件使用一种类似于C语言结构体的方式进行描述,统一使用无符号整数作为其基本数据类型;
      • u1,u2,u4,u8分别表示无符号单字节,2字节,4字节,8字节整数;
      • 字符串使用ul数组进行表示;
    • Class文件可以被严谨地描述为:

    • Class文件的结构严格按照该结构体定义:
      • 文件以一个4字节的Magic(被称为魔数)开头,紧跟着两个大小版本号;
      • 在版本号之后是常量池,常量池的个数为contant_pool_count,常量池中的表项有constant_pool_count-1项;
      • 常量池之后是类的访问修饰符,代表自身类的引用,父类引用以及接口数量和实现的接口引用;
      • 在接口之后,有着字段的数量和字段描述,方法数量以及方法的描述;
      • 最后,存放着类文件的属性信息;
  • Class文件的标志---魔数
    • 作用:它用来通知Java虚拟机,这是一个Class文件;
    • 结构:它是一个4字节的无符号整数,它固定为0xCAFEBABE
    • 示例1:如果Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
    • 示例2:展示魔数的内容

public class SimpleUser {

public static final int TYPE =1;

private int id;

private String name;

public int getId() {

return id;

}

public void setId(int id) throws IllegalStateException{

try{

this.id = id;

} catch(IllegalStateException e){

System.out.println(e.toString());

}

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

使用软件WinHex打开以上代码生成的Class文件,可以很容易看到魔数:

  

  • Class文件的版本
    • 作用:在魔数后面,紧跟着Class的大小版本号。这表示当前Class文件,是由哪个版本的编译器编译产生的。首先出现的是小版本号,是一个两字节的无符号整数,在此之后的大版本号,也用两个字节表示;
      • 版本号和Java编译器的对应关系如表:
    • 示例1:如下图,大版本号为0x33换算为16进制为51,因此可以判断该Class文件是由JDK1.7的编译器,或者在JDK1.8下,使用"-target1.7"参数编译生成的;

    • 示例2:低版本的Java虚拟机不能执行高版本编译器生成的Class文件,下图显示的异常这类错误
  • 存放所有常数--常量池
    • 在版本号之后,紧跟着的是常量池的数量,以及若干常量池表项;
    • 示例10x37表示该Class文件中合计有常量池表项55-1=54项(常量池0为空缺项,不存放实际内容,0x37换算了10进制为55.在数量之后,就是常量池的实际内容,每一项以类型,长度,内容或者类型,内容的格式依次排列
    • 常量池底层的数据类型CONSTANT_Utf8utf8字符串),CONSTANT_Integer(整数),CONSTANT_Float(浮点数),CONSTANT_Long(长整型),CONSTANT_Double(双精度浮点类型);
      • CONSTANT_Utf8的格式:

UTF8tag值为1,紧接着,字符串的长度length,最后是字符串的内容

  • 示例2:0x01表示一个UTF8常量,0x0019 表示该常量一共25个字节。因此,从0x0019之后数25个字节就为该常量的实际内容

  • UTF8的常量经常被其他类型的常量引用,CONSTANT_Class的结构如下:

tag7,表示一个CONSTANT_Class常量,第2个字段为一个2字节的整数,表示常量池的索引,
CONSTANT_Class中,该索引指向的常量必须是CONSTANT_Utf8;

  • CONSTANT_Integer,CONSTANT_Float,CONSTANT_Long,CONSTANT_Double分别表示数字的字面量。当使用final定义一个数字常量时,Class文件中就会生成一个数字的常量,它们的结构分别为:

对于CONSTANT_IntegerCONSTANT_Float,它们的值由一个4字节的无符号整数表示;

对于CONSTANT_LongCONSTANT_Double
它们的值由两个4字节无符号整数表示。

  • CONSTANT_String表示一个字符串常量

其中tag8,一个2字节长的无符号整数指向常量池的索引,表示该字符串对应的UTF8内容

  • CONSTANT_NameAndType,表示一个名词和类型

tag12,第一个2字节name_index表示名称,意为常量池的索引,表示常量池第name_index项为名字,通常可以表示字段名字或者方法名字。
第二个2字节descriptor_index表示类型的描述,比如表示方法的签名或者字段的类型;

  • 对于类的方法和字段,分别使用CONSTANT_MethodrefCONSTANT_Fieldref表示。它们分别表示一个类的方法以及字段的引用。他们的结构很类似:

 

CONSTANT_Methodreftag值为10CONSTANT_Fieldreftag值为9

它们的class_index表示方法或者字段所在的类在常量池中的索引,它会指向一个CONSTANT_Class结构。
2name_and_type_index也是指向常量池的索引,但表示一个CONSTANT_NameAndType结构,它定义了方法或者字段的名称,类型或者签名.

右图示例:System.out.println()表示的CONSTANT_Methodref在常量池中的引用关系。Methodref结构的class index字段指向了第41号常量池项,表示Class,而该项又进一步指向常量池中的UTF8数据,表明该Class的类型。

Methodrefname and type字段则指向常量池第43项,NameAndType类型的数据,它包括名字和类型两个字段,有指向常量池中的两个字符串println和(Ljava/lang/String;V,表示方法的名字和方法的签名。

Ljava/lang/String;V表示该方法接收一个String类型的参数,并且返回值为void

这样,通过常量池中的引用关系,通过Methodref结构,将方法描述清楚。

  

字段CONSTANT_FieldrefCONSTANT_Methodref是完全类似的;

  • CONSTANT_InterfaceMethodref,表示一个接口方法。在Java程序中,出现了对接口方法的调用,那么就会在常量池中生成一个接口方法的引用,结构如下:

它的使用方式和CONSTANT_Methodref是一样的

  • CONSTANT_MethodHandle,CONSTANT_MethodTypeCONSTANT_lnvokeDynamic。它们是在JDK1.7引入的新的常量类型,分别表示函数句柄,函数类型签名和动态调用
    • CONSTANT_MethodType的结构定义如下:

tag为16,descriptor_index为指向常量池的一个UTF8字符串的索引,使用方式与前面的索引一致。
该常量项用于描述一个方法签名,比如"()V",表示一个不接收参数,返回值为void的方法。当需要传送给引导方法一个MethodType类型时,类文件中就会出现此项

  • CONSTANT_MethodHandle为一个方法句柄,它可以用来表示函数方法,类的字段或者构造函数等。方法句柄指向一个方法,字段和C语言中的函数指针或者C#中的委托类似

 

tag为15,reference_kind表示这个方法句柄的类型,reference_index为指向常量池的索引。

reference_index具体指向的类型,由reference_kind确定

MethodHandle字段的对应关系表

  • CONSTANT_InvokeDynaimic结构用于描述一个动态调用,动态调用是Java虚拟机为动态语言,调用动态函数专门提供的;

tag18bootstrap_method_attr_index为指向引导方法表中的索引,用来定位到一个引导方法。

引导方法用于在动态调用时进行运行时函数查找和绑定。

引导方法表属于类文件的属性(Attribute),name_and_type_index为指向常量池的索引,且指向的表项必须是CONSTANT_NameAndType,用于表示方法的名字以及签名。

  • Class的访问标记(Access Flag
    • 常量池后,紧跟着访问标记。该标记使用两个字节表示,用于表明该类的访问信息,如public,final,abstract

每种类型都是通过设置访问标记的32位特定位来表示的。

例:该标记为0x0021,因此,可以判断该类为public,且ACC_SUPER标记被置为1;

说明:使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),
现代编译器都会设置并且使用这个标记

  • 当前类,父类和接口
    • 在访问标记后,会指定该类的类别,父类类别以及实现的接口,格式如下:

this_class,super_class都是2字节无符号整数,它们指向常量池中一个CONSTANT_Class,以表示当前的类型以及父类。
Java支持单继承,只需要保存单个父类即可。
注意:super_class指向的父类不能是final;

  • 类可以实现多个接口,需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(这里必须是接口,而不是类)
  • 如果该类没有实现任何接口,则interfaces_count为0;
  • Class文件的字段
    • 接口描述后面,会有类的字段信息。由于一个类会有多个字段,要先指明字段个数;

字段的数量fields_count是一个2字节无符号整数。

  • 字段数量之后为字段的具体信息,每个字段为一个field_info的结构,该结构如下:

    • 一开始是字段的访问标记,类似于类的访问标记,该字段的取值如下:
    • 之后,是一个2字节整数,表示字段的名称,它指向常量池中的CONSTANT_Utf8结构;
    • 名称后的descriptor_index也指向常量池中CONSTANT_Utf8,该字符用于描述字段的类型;
    • 一个字段可以配置一些属性,用来存储额外的信息,比如初始值,注释等;
      • 属性个数在attributes_count中,属性内容存放在attributes数组中;
      • 以常量属性为例,常量属性的结构为:

 

常量属性的attribute_name_index为2字节整数,指向常量池的CONSTANT_Utf8,并且这个字符串为"ConstantValue"。
之后的attribute_length,它由4个字节组成,表示这个属性的剩余长度;对于常量属性来说,它的值恒定为2;
最后的constantvalue_index表示属性值,但值并不直接出现在属性中,而是存放在常量池中,这里的constantvalue_index也是指向常量池的索引,并且存在下表的对应的关系。这表示,一个int类型字段的常量,constantvalue_index指向的常量池类型必须是CONSTANT_Integer

  • Class文件的方法基本结构
    • 字段之后,是类的方法信息。方法信息由两部分组成

其中,methods_count2字节整数,表示该类中有几个方法。

接着就是methods_countmethod_info结构,每一个method_info表示一个方法

  • method_info的结构

    • access_flag为方法的访问标记,用于标明方法的权限以及相关特性,它的取值如下:

  • name_index表示方法的名称,它是一个指向常量池的索引;
  • descriptor_index为方法描述符,它是指向常量池的索引,是一个字符串,用于表示方法的签名(参数,返回值),还对方法的签名做了一些规定;
    • 它将函数的参数类型写在一对小括号中,并在括号右侧给处方法的返回值;
    • 例:object m(int i,double d,Thread t){....},则它的方法描述符为:

      (IDLjava/lang/Thread;)Ljava/lang/object;

      • 方法的参数统一列在一对小括号中,"I"表示int,"D"表示double,"Ljava/lang/Thread;"表示Thread对象。小括号右侧的Ljava/lang/object;表示方法的返回值为Object对象;
  • attributes_count表示方法中属性的数量,紧接着,就是attributes_count个属性的描述;
  • 对于attirbutes来说,它们的统一格式为:

attribute_name_index:当前的attribute名称
attribute_length:当前attribute的剩余长度
info[attribute_length]:多少个子节的byte数组

  • 常用Attribute属性

  • 方法的执行主体--Code属性
    • 方法的内容存在属性中,这里最重要的属性是Code。它存放着方法的字节码等信息:
      • attribute_name_index:该属性的名称,是指向常量池的索引,指向的类型为CONSTANT_Utf8;
      • attribute_length:指定了Code属性的长度,该长度不包括前6个字节,也就是剩余长度;
      • max_stack:操作数栈的最大深度;
      • max_locals:局部变量表的最大值;
      • code_length和code[code_length]:方法的字节码,前者为长度,后者为byte数组,是字节码内容本身;
      • 字节码之后是该方法的异常处理表,它告诉一个方法该如何处理字节码中可能抛出的异常;它由表项数量和内容组成;
        • exception_table_length:表示异常表的表项数量;
        • exception_table[exception_table_length]:结构为异常表;
        • 表中每一行由4部分组成,分别是start_pc,end_pc,handler_pc,catch_type;
          • 它们表示:从方法字节码的start_pc偏移量开始到end_pc偏移量为止的这段代码中,如果遇到了catch_type所指定的异常,那么代码就跳转到hadler_pc的位置执行。
          • 它们4个中:start_pc,end_pc,handler_pc都是字节码的编译量,也就是code[code_length]中的位置,而catch_type为指向常量池的索引,它指向一个CONSTANT_Class类,表示需要处理的异常类型;
    • 属性总览
      • 其作用与结构为下图:

  • 记录行号--LineNumberTable属性
    • LineNumberTable是Code属性本身保存的一些额外信息。它用于描述Code的Code属性;
    • LineNumberTable用来记录字节码偏移量和行号的对应关系。若没有它,则调试器无法定位到对应的源码;
    • 属性结构如下:

attribute_name_index:指向常量池的索引;在LineNumberTable属性中,该值为"LineNumberTable";

attribute_length:4字节无符号整数,表示属性的长度(不含前6个字节);

line_number_table_length:表项有多少条记录;

line_number_table:表的实际内容;它包含line_number_table_length个<start_pc,line_number>元组,start_pc为字节码偏移量,line_number为对应的行号;
 

  • 保存局部变量和参数--LocalVariableTable属性
    • 它记录一个方法中所有的局部变量

attribute_name_index:当前属性的名字,是指向常量池的索引;
attribute_length:属性的长度;
local_variable_table_length:局部变量表表项条目;

  • 局部变量表的每一条记录由以下几个部分组成:
    • start_pc,length:表示当前局部变量的开始位置(start_pc)和结束位置(start_pc+length,不含最后一个字节);
    • name_index:局部变量的名称,这是一个指向常量池的索引;
    • descriptor_index:局部变量的类型描述,指向常量池的索引;使用和字段描述符一样的方式描述局部变量;
    • index:局部变量在当前帧栈的局部变量表中的槽位。对于long和double的数据,它们会占据局部变量表中的两个槽位;
  • 加快字节码校验--StackMapTable属性
    • JDK1.6之后,方法的Code属性还可能包含一个StackMapTable的属性结构;该结构中存有若干个叫做栈映射帧的数据;该属性不包含运行时所需的信息,仅用作Class文件的类型校验;
      • 结构如下:

attribute_name_index:常量池索引,恒为"StackMapTable",attribute_length为该属性的长度;
attribute_length:该属性的长度;
number_of_entries:栈映射帧的数量,最后的entries则为具体的内容,每一项为一个stack_map_frame结构

  • 栈映射帧的作用
    • 每个栈映射帧都为了说明在一个特定的字节码偏移位置上,系统的数据类型是什么(包括局部变量表的类型和操作数栈的类型);
    • 每一帧都会显式或者隐式地指定一个字节码偏移量的变化值offset_delta,使用offset_delta可以计算出这一帧数据的字节码偏移位置;
    • 计算方法为offset_delta+1和上一帧的字节码偏移量相加。如果上一帧是方法的初始帧,那么,字节码偏移量为offset_delta本身;
    • 注意:这里说的"帧",和栈帧的帧不是同一个概念。这里更接近与一个跳转语句,跳转语句将函数划分成不同的块,每一块的概念就接近与这里所说的栈映射帧中的"帧";
  • StackMapTable结构中的stack_map_frame被定义为一个枚举值,它可能的取值如下:

    • same_frame:表示当前代码所在位置和上一个比较位置的局部变量表是完全相同的,并且操作数栈为空。它的取值为0~63,这个取值也是隐含的offset_delta,表示距离上一个帧块的偏移量;

  • same_locals_1_stack_item_frame:frame_type的范围为:64-127,如果栈映射帧为该值,则表示当前帧和上一帧有相同的局部变量,且操作数栈中变量数量为1.
    • 它有一个隐式的offset_delta,使用frame_type-64可以计算出来。之后verification_type_info,表示该操作数中的变量类型;

  • same_locals_1_stack_item_frame_extended与same_locals_1_stack_item_frame含义相同,区别在于:
    • 前者表示的offset_delta如果超出范围,需要使用same_locals_1_stack_item_frame_extended;
    • same_locals_1_stack_item_frame_extended使用显示的offset_delta;
    • 结构的最后,存放着操作数栈的数据类型:
  • chop_frame表示操作数栈为空,当前局部变量表比前一帧少K个局部变量;
  • same_frame_extended与same_frame含义一致。表示局部变量信息和上一帧相同,且操作数栈为空。但是same_frame_extended显示指定了offset_delta,可以表示更大的字节偏移量;
  • append_frame表示当前帧比上一帧多了K个局部变量,且操作数栈为空。在append_frame的最后,还存放着增加的局部变量的类型:
  • 如果以上结构无法表达帧的信息时,可以使用full_frame来表示。它将局部变量表和操作数栈做了完整的记录:

full_frame中,显示指定了offset_delta,完整记录了局部变量表的数量(number_of_locals),局部变量表的数据类型(locals),操作数栈的数量(number_of_stack_items)和操作数栈的类型(stack)

  • 抛出异常--Exceptions属性
    • 每个方法都可以有一个exceptions属性,用于保存该方法可能抛出的异常信息;

Exceptions与Code属性中的异常表不同。Exceptions属性表示一个方法可能抛出的异常,通常是由方法的throws关键字指定。而Code属性中的异常表,则是异常处理机制,由try-catch语句生成

  • 示例:下段代码将会生成带有Exceptions属性的方法

public static void main(String[] args) throws java.io.IOException{}

  • Exceptions属性
    • attribute_name_index:属性的名称,指向常量池的索引,恒为:"Exceptions";
    • attribute_length:属性长度;
    • number_of_exceptions:表项数量即可能抛出的异常个数;
    • exception_index_table:罗列了所有的异常,每项为指向常量池的索引,对应的常量为CONSTANT_Class,为一个 异常类;
  • SourceFile属性
    • 该属性属于Class文件的属性。用于描述当前这个Class文件是由哪个源代码文件编译得来的:

attribute_name_index:属性名称,指向常量池的一个索引,这里恒为"SourceFile";
attribute_length:属性长度;对于SourceFile属性来说,恒为2;
sourcefile_index:表示源代码文件名,它是指向常量池的索引,为CONSTANT_Utf8类型;

  • 动态调用--BootstrapMethods属性
    • 为了支持JDK1.7的invokeDynamic指令,Java虚拟机增加了BootstrapMethods属性,它用于描述和保存引导方法。
      • 引导方法可以理解成是一个查找方法的方法,invokeDynamic需要能够在运行时根据实际情况返回合适的方法调用,而使用何种策略去查找所需要的方法,是由引导方法决定的;
      • BootstrapMethods属性就是用于找到调用的目标方法;
      • 该属性是Class文件的属性;

attribute_name_index:属性名称,为指向常量池的索引,这里是"BootstrapMethods";
attribute_length:4字节码数字,表示属性的总长度(不含这前6个字节);
num_bootstrap_methods:这个类中包含的引导方法的个数;
最后是num_bootstrap_methods个bootstrap_methods引导方法;

  • 每个引导方法又由3个字段构成,含义如下:
    • bootstrap_method_ref:必须是指向常量池的常数,并且入口为CONSTANT_MethodHandle,用于指名函数;
    • num_bootstrap_arguments:指明引导方法的参数个数;
    • bootstrap_arguments:引导方法的参数类型。这是指向常量池的索引,且常量池入口只能是:CONSTANT_String,CONSTANT_Class,CONSTANT_Integer,CONSTANT_Long,CONSTANT_Float,CONSTANT_Double,CONSTANT_MethodHandle,CONSTANT_MethodType;
      • 引导方法只接受以上类型的参数;
  • 内部类--InnerClasses属性
    • InnerClass属性是Class文件的属性,用来描述外部类和内部类的之间的联系;

attribute_name_index:属性名称,指向常量池索引,恒为"InnerClasses";
attribute_length:为属性长度;
number_of_classes:表示内部类的个数;
classes[number_of_classes]:描述内部类的表格,每条内部类记录包含4个字段:

inner_class_info_index:指向常量池的指针,指向一个CONSTANT_Class,表示内部类的类型;

outer_class_info_index:表示外部类,也是常量池的索引;

inner_name_index:表示内部类的名称,指向常量池中的CONSTANT_Utf8项;

inner_class_access_flags:内部类的访问标识符,用于指示static,public等属性

  • 常用的Attribute属性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值