虚拟机类加载机制(二)--- 类文件结构之属性表详解

Java的技术体系包括

  • 支持Java程序运行的虚拟机(JVM)
  • 提供接口支持的Java API
  • Java 编程语言
  • 第三方Java框架(如Spring等)

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


上一篇文章我们详细介绍了由java类编译而成的Class文件的结构,其中最后一项属性表集合(attribute_info)内容较多,且我们使用Java编写的代码逻辑,大部分都在此表示,因此单独拿出一篇文章详细介绍属性表的内容。在第七版的虚拟机规范中已经预定了21项属性表,且还可以自定义属性表,因此属性表很多,我们详细了解一下其中最为常用的几项

Code属性

一个类中的方法中的代码,经过编译之后,最终变为字节码指令存储在Code属性内。Code属性位于方法表(method_info)的属性集合之中,但并非所有的方法都有这个属性,比如接口或者抽象类的方法就没有。Code属相的结构如下图所示

类型 名称 数量 描述
u2 attribute_name_index 1 属性名称
u4 attribute_length 1 属性总长度
u2 max_stack 1 操作数栈最大深度
u2 max_locals 1 局部变量表所需要的存储空间,编译器会根据局部变量的作用域等计算出该值的大小
u4 code_length 1 字节码长度
u1 code code_length 用来存储字节码指令的一系列字节流
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_table_length 显示异常处理表
u2 attribute_count 1
attribute_info attributes attributes_count

其中code和code_length用来表示Java程序编译后生成的字节码指令。code_length代表字节码的长度,code用于存储字节码指令的一系列字节流。字节码指令,顾名思义,就是一个字节代表的指令,一个字节代表的值范围是0~255,也就是说总共可以表达256种指令,目前虚拟机规范定义了大约200条指令。code_length是一个u4类型的数据,理论上表达的意思是字节码指令的最大长度可以达到2的32次方-1,但虚拟机规范中明确规定一个方法不允许超过65535条字节码指令,也就是两个字节所能表示的最大值。

max_locals中的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位,对于长度不超过32位的数据类型,每个局部变量表占用1个Slot,如byte, char, float, int, short, boolean, returnAdress等。对于double和long这两种64位数据类型需要两个Slot

Code属性是Class文件中最最重要的一个属性。因为一个Java类中表达的信息,我们可以将其分为两部分元数据(包括类、字段、方法定义和其他信息)和代码逻辑(方法中的代码),那么在整个Class文件中,Code 属性用来描述代码逻辑,其他的数据项均是来描述元数据的。换句话讲,我们所写代码的主体逻辑,告诉计算机该做什么事情,都是在Code属性中包含。

异常表的格式如下所示,具体含义为,如果当字节码在第start_pc行到end_pc行之间出现了catch_type类型或者其子类的异常,则转到第handler_pc行继续执行。

类型 名称 数量 类型 名称 数量
u2 start_pc 1 u2 handler_pc 1
u2 end_pc 1 u2 catch_type 1

Exceptions 属性

这里的Exception属性是在方法表中与Code属性平级的一项属性,表示方法描述时,在throws关键字后面列举的异常。结构如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

LineNumberTable属性

LineNumberTable属性用于描述java代码行号与字节码行号(字节码的偏移量)之间的对应关系。它不是运行时必须的属性。如果没有这项属性,不会对程序的运行产生任何影响,但是当程序抛出异常时,堆栈中没有出错的行号,而且在调试程序时,也无法按照源码行来调试断点。默认会在Class文件中生成这一项,可以使用如下命令取消生成

javac -g:none或者-g:lines

它的结构如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

其中line_number_info表包含了start_pc和line_number两个u2类型的数据,前者是字节码行号,后者是源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量和java源码中的变量之间的关系。它也不是运行时必须的属性,但默认会生成到Class文件中。如果没有这项属性,当别人调用这个方法的时候,所有参数名称都会丢失,被arg0,arg1之类的占位符代替,会对代码编写带来不方便。可以使用如下命令取消生成

javac -g:none或者-g:vars

它的结构如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info lacal_variable_table local_variable_table_length

其中local_variable_info表示一个栈帧与源码中的局部变量的对应关系,结构和含义如下所示

类型 名称 数量 描述
u2 start_pc 1 局部变量生命周期开始的字节码偏移量
u2 length 1 作用范围的长度
u2 name_index 1 局部变量名称,指向常量池的索引
u2 descriptor_index 1 局部变量描述符,指向常量池的索引
u2 index 1 局部变量在栈帧中局部变量表中Slot的位置

SourceFile属性

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。可使用如下命令取消生成这项属性

javac -g:none或者-g:source

这个属性是一个定长的属性,它的结构如下

类型 名称 数量 描述
u2 attribute_name_index 1
u4 attribute_length 1
u2 source_index 指向常量池中CONSTANT_Utf8_info类型常量的索引,常量值是源码文件的文件名

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。对于静态变量,有两种时刻可以赋值,一种是在类构造器,另一种是使用ConstantValue属性。各种编译器的实现可能不同。我们常用的Sun javac编译器的选择是,对于同时使用final和static的常量,并且它的类型是基本类型或者String类型,就生成ConstantValue来赋值。如果没有被final修饰,或者非基本类型或者字符串,则会选择在中进行初始化。从虚拟机规范的角度来讲,ConstantValue属性并没有要求必须设置了ACC_FINAL标志,而是要求了必须设置有ACC_STATIC标志。

该属性的结构如下,它也是一个定长的属性

类型 名称 数量 描述
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantcalue_index 指向常量池中的索引

InnerClass属性

InnerClass属性用于记录内部类与宿主之间的关联。如果一个类中定义了内部类,编译器就会分别在这个类和它的内部类中生成该属性。该属性的结构如下

类型 名称 数量 描述
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1 记录有多少个内部类
inner_classes_info inner_classes 1 每个内部类是一个该类型的表

inner_classes_info的结构如下

类型 名称 数量 描述
u2 inner_class_info_index 1 指向常量池中CONSTANT_Class_info型常量,代表内部类的符号引用
u2 outer_class_info_index 1 指向常量池中CONSTANT_Class_info型常量,代表外部类的符号引用
u2 inner_name_index 1 指向常量池中CONSTANT_Utf8_info型常量,代表内部类的名称
u2 inner_class_access_flags 1 代表内部类的访问标志,类似于access_flags

其中inner_class_access_flags标志位的含义如下表

标志名称 标志值 含义
ACC_PUBLIC 0x0001 内部类是否为public
ACC_PRIVATE 0x0002 内部类是否为private
ACC_PROTECTED 0x0004 内部类是否为protected
ACC_STATIC 0x0008 内部类是否为static
ACC_FINAL 0x0010 内部类是否为final
ACC_SYNCHRONIZED 0x0020 内部类是否为synchronized
ACC_ABSTRACT 0x0400 内部类是否为abstract
ACC_SYNTHETIC 0x1000 内部类是否由编译器自动产生
ACC_ANNOTATION 0x2000 内部类是一个注解
ACC_ENUM 0x4000 内部类是一个枚举

Deprecated 与 Synthetic 属性

这两个属性都属于标志类型的布尔属性,要么有要么没有,不存在属性值的概念。其中,Deprecated用于表示某个类或者方法字段已经被认定为不推荐使用,它可以通过在代码中使用 @deprecated 注释进行设置。Synthetic属性代表某个字段或者方法不是由java源代码产生,而是由编译器自动添加的。这两个属性的结构如下,其中attribute_length的值必须为0。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

编译器自动生成的实例构造器和类构造器除外

StackMapTable属性

该属性是JDK1.6之后增加到Class文件规范中的,它是一个复杂的变长属性,位于Code属性表中。它的功能用于,在类加载的字节码验证阶段用于验证字节码逻辑合法性,它的工作原理在虚拟机规范中用了整整120页来讲解,因此十分复杂。我们只需要知道该属性的含义就够了。它的结构如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

虚拟机规范中规定,在Class版本号大于50的文件中,如果方法的Code属性中没有StackMapTable属性,则表示它带有一个隐式的StackMap属性。这个StackMapTable属性的作用等同于number_of_entries值为0的属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则会抛出ClassFormatError异常。

Signature属性

Signature属性在JDK1.5之后增加到了Class文件中,它是一个可选的定长属性,可以出现在类、字段、方法的属性表中。它的作用是,任何类、接口、初始化成员或者方法的泛型签名如果包含了类型变量或参数化类型,则Signature会为它记录泛型签名信息。它的在结构如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

其中signature_index项的值是一个对常量池的索引。常量池在该索引处的项是一个CONSTANT_Utf8_info结构,表示类签名,方法类型签名或者字段签名。

你可能感兴趣的:(虚拟机类加载机制(二)--- 类文件结构之属性表详解)