龙空技术网

来聊聊基础——Java字节码属性

架构宅话 660

前言:

当前小伙伴们对“java基本属性”可能比较关怀,各位老铁们都想要学习一些“java基本属性”的相关内容。那么小编也在网摘上收集了一些关于“java基本属性””的相关知识,希望我们能喜欢,我们快快来了解一下吧!

1. 什么是属性

字节码中的属性(attribute)是存在于class文件中ClassFile结构、field_info结构、method_info结构和code_attribute结构的具有特定格式的数据,用来描述类、字段、方法或代码块的一些附加信息。这些附加信息有些是必要的(例如方法体的指令序列),有些是非必要的(例如行号表、局部变量表等调试信息)。

与class文件中其它数据项要求严格的顺序、长度和内容不同,属性的限制稍微宽松一些,不再要求各个属性具有严格的顺序。在Java 8中,《Java虚拟机规范》预定义了23种字节码属性,按它们在class文件中出现的顺序列在下表中:

属性名

class 文件

Java SE

出现位置

作用

ConstantValue

45.3

1.0.2

field_info

被final关键字修饰的常量值

Code

45.3

1.0.2

method_info

方法体编译成的指令序列

Exceptions

45.3

1.0.2

method_info

方法声明时显式抛出的异常列表

SourceFile

45.3

1.0.2

ClassFile

记录编译生成该class文件的源文件名称

LineNumberTable

45.3

1.0.2

Code属性

行号表,记录字节码指令与源码行号的对应关系

LocalVariableTable

45.3

1.0.2

Code属性

局部变量表,方法内局部变量的描述

InnerClasses

45.3

1.1

ClassFile

内部类列表

Synthetic

45.3

1.1

ClassFile、method_info、field_info

表示该实体是由编译器生成,而非在源码中实际存在

Deprecated

45.3

1.1

ClassFile、method_info、field_info

由@Deprecated注解标识,表示已被弃用

EnclosingMethod

49.0

5.0

ClassFile

仅当一个类为局部内部类或匿名类时才有该属性,表示该类所在的外围方法

Signature

49.0

5.0

ClassFile、method_info、field_info

表示类、方法或字段上的签名字符串。JDK 5中引入了泛型,但是为了向后兼容在编译时会进行范型擦除还原为原始类型。为了记录记录泛型信息引入了该属性

SourceDebugExtension

49.0

5.0

ClassFile

JDK 5中新增的属性,用于存储额外的调试信息。例如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR 45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性可以存储这个标准所新加入的调试信息

LocalVariableTypeTable

49.0

5.0

Code属性

对于使用了类型变量(type variable)或参数化类型(parameterized type)的局部变量,用来描述它们的泛型信息

RuntimeVisibleAnnotations

49.0

5.0

ClassFile、method_info、field_info

运行时可见的注解列表(在运行时可以通过反射访问到)

RuntimeInvisibleAnnotations

49.0

5.0

ClassFile、method_info、field_info

运行时不可见的注解列表(在运行时通过反射无法访问到)

RuntimeVisibleParameterAnnotations

49.0

5.0

method_info

运行时可见的方法参数上的注解列表

RuntimeInvisibleParameterAnnotations

49.0

5.0

method_info

运行时不可见的方法参数上的注解列表

AnnotationDefault

49.0

5.0

method_info

注解类元素的默认值(使用default关键字定义)

StackMapTable

50.0

6

Code属性

栈映射表,供类型检查校验器(type verifier)使用,用来在运行阶段校验线程栈中局部变量表和操作数栈中元素的类型是否匹配

BootstrapMethods

51.0

7

ClassFile

JDK 7中新增的属性,用来支持动态类型的语言

RuntimeVisibleTypeAnnotations

52.0

8

ClassFile、method_info、field_info、Code属性

为实现JSR 308中新增的类型注解(type annotation)特性提供的支持,用于指明哪些注解类在运行时可见的(可以通过反射提取)。详见:

RuntimeInvisibleTypeAnnotations

52.0

8

ClassFile、method_info、field_info、Code属性

与RuntimeVisibleTypeAnnotations相反,用于指明哪些注解类在运行时不可见的(通过反射无法提取)

MethodParameters

52.0

8

method_info

记录方法参数的真实名称。正常javac编译器是不会记录方法的参数名的,但通过 -parameters 参数可以将参数名编译进class文件并能在运行时通过反射访问到

除了上述预定义的属性外,编译器也可以在ClassFile结构、field_info结构、method_info结构以及Code的属性表里生成新的属性。Java虚拟机的实现版本可以使用找到的新定义的属性,但如果不能识别的话会自动忽略。

所有的属性都具有如下通用格式:

类型

字段名

含义

u2

attribute_name_index

指向常量池中类型为CONSTANT_Utf8_info的16位无符号索引,表示该属性的名称

u4

attribute_length

给出了紧随其后的数据字节的长度,这个长度不包括 attribute_name_index 和 attribute_length 项的6个字节

u1[attribute_length]

info

该属性的真实信息

2. 预定义属性

本节对JDK 8中预定义的属性进行比较详细的说明。

2.1 ConstantValue属性

ConstantValue是一个存在于field_info结构的属性表中的固定长度的属性,用来表示一个常量常量表达式的值。其在虚拟机中用法如下:

如果field_info结构的access_flags项中的ACC_FINAL标志被设置(说明对应的字段由final修饰),那么在声明该字段的类或接口初始化时,该字段的值会被设置为由ConstantValue表示的值,并且该赋值过程发生在类或接口的初始化方法(“<clinit>”方法)调用之前。否则,Java虚拟机会忽略该属性。

ConstantValue属性具有如下格式:

ConstantValue_attribute {    u2 attribute_name_index;     u4 attribute_length;         // must be two    u2 constantvalue_index; }

ConstantValue_attribute结构的各项说明如下:

attribute_name_index:指向常量池的有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“ConstantValue”。attribute_length:该项的值必须为2。constantvalue_index:constantvalue_index 是指向常量池表(constant_pool table)的一个有效索引,该常量池项表示属性的常量值。常量池项的类型与字段的类型必须匹配,如下表所示:

字段类型

常量池项类型

long

CONSTANT_Long

float

CONSTANT_Float

double

CONSTANT_Integer

int, short, char, byte, boolean

CONSTANT_Integer

String

CONSTANT_String

从上表中可知,byte、char、boolean、short四个长度不足4字节的基本类型的值会被编译成int类型,并且这4种类型在Java虚拟机中也是以int类型表示和运算的。

2.2 Code属性

Code是一个位于method_info结构体的属性表中可变长度的属性。Code属性包含方法(包括实例初始化方法以及类和接口的初始化方法)的字节码指令以及一些额外的辅助信息。

如果一个方法为 native 或 abstract 方法,那么它的属性表中一定不含有Code属性,在其它情况下,method_info中必须有且只能有一个Code属性。

Code属性具有如下格式:

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]; }

Code_attribute各项的说明如下:

attribute_name_index:指向常量池表的一个有效索引,该索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“Code”。attribute_length:表示该属性的长度,不包括起始的6个字节。max_stack:表示该方法执行过程中,操作数栈的最大深度。max_locals:给出了本方法调用时局部变量数组的长度,包括作为方法入参的局部变量。max_stack 和 max_locals 是编译器在编译源代码时生成的,在方法被调用创建栈桢时使用,作为操作数栈和局部变量表分配的依据。 注意 long 和 double 基本类型占用两个slot(槽,一个槽占4个字节)。code_length:指定该方法的字节码指令序列的长度。code[]:code数组,给出该方法实际的字节码指令。exception_table_length:给出 exception_table 表的成员个数。exception_table[]:异常处理器表,该数组中的每一项都表示code数组中的一个异常处理器(exception handler)。exception_table 数组中处理器出现的顺序并非无关紧要的(这也就是为什么 try... catch... 代码块中多个catch子句的顺序对程序执行结果产生影响,catch子句会被编译成异常处理器按顺序保存在 exception_table中,并在方法抛出异常时按顺序被查找)。

exception_table中的每一个处理器又包含如下4项:

start_pc, end_pc:这两项表示该异常处理器在code数组上作用的范围([start_pc, end_pc) 组成的半开区间)。start_pc必须是指向一条指令的opcode(操作码)的有效索引,而end_pc也必须是指向某条指令的opcode的有效索引,或者等于code_length,也即code数组的长度。handler_pc:该项的值表示处理器在code数组的起始位置。catch_type:如果该项的值不为0,则它必须是指向 constant_pool 的一个有效索引,该索引处的常量池项必须是一个CONSTANT_Class_info结构,表示该异常处理器可以捕获的异常类型。只有在抛出的异常为指定类型或指定类型的子类时,才会跳转到该异常处理器。

如果catch_type项的值为0,该异常处理器在抛出任何异常时都会执行,该特性是为了实现 finally 语义。

从异常处理器的结构可以看出,当方法运行时如果 [start_pc, end_pc) 之间抛出由catch_type指定的异常,会跳转到handler_pc指定的指令处处理该异常。

attribute_count:表示该Code属性的属性个数。attributes[]:一个Code属性可以有任意多可选的选项与之关联,可以参考第一节中关于预定义属性的介绍,查看哪些属性可以出现在Code属性的属性表中。2.3 Exceptions属性

Exceptions是存在于methid_info结构的属性表中的一个可变长度的属性,用来表示一个方法可能抛出的异常列表(虽然官方文档说是受检异常,但事实证明非受检异常也会在该属性中列出)。

Exceptions属性具有如下格式:

Exceptions_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 number_of_exceptions;    u2 exception_index_table[number_of_exceptions];}

Exceptions_attribute结构的每一项说明如下:

attribute_name_index:该项必须是一个指向常量池的有效索引,索引处的值是一个CONSTANT_Utf8_info的结构,表示字符串“Exceptions”。attribute_length:本属性的长度,不包括起始的6个字节。number_of_exceptions:表示 exception_index_table 表中成员的个数。exception_index_table[]:该数组的每一项都必须是指向常量池的有效索引,索引处的值是一个CONSTANT_Class_info结构,表示方法声明时显式抛出的异常的类型。2.4 SourceFile属性

SourceFile是ClassFile结构中一个可选的固定长度的属性,用来表示本class文件所属的源码文件的名称。SourceFile属性具有如下格式:

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

SourceFile_attribute结构中的每一项说明如下:

attribute_name_index:该项是一个指向常量池的有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“SourceFile”。attribute_length:该项的值必须为2(即 sourcefile_index 项的长度)。sourcefile_index:该项是一个指向常量池的有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示源码文件名称的字符串。2.5 LineNumberTable属性

LineNumberTable是Code属性的属性表中一个可选的可变长度的属性,它可以帮助调试器确定code数组中哪一部分与源码文件行号的对应关系。

LineNumberTable属性具有如下格式:

LineNumberTable_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 line_number_table_length;    {        u2 start_pc;        u2 line_number;    } line_number_table[line_number_table_length];}

LineNumberTable_attribute结构的各项说明如下:

attribute_name_index:该项必须是一个指向常量池的有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“LineNumberTable”。attribute_length:表示本属性的长度,不包括起始的6个字节。line_number_table_length:给出 line_number_table数组的成员个数。line_number_table[]:line_number_table数组中的每一项都表示源码文件的行号发生了变化及其对应的code数组的位置(也即源码文件的行号发生了变化都会在line_number_table数组中生成一项)。该数组的每一个元素又包含如下两项:start_pc:必须是指向code数组的索引,表示源码文件新的一行开始。line_number:给出对应于源码文件的行号。2.6 LocalVariableTable属性

LocalVariableTable是Code属性的属性表中一个可选的可变长度的属性,它可以帮助调试器确定一个方法在执行过程中某个给定局部变量的值。

LocalVariableTable属性具有如下格式:

LocalVariableTable_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 local_variable_table_length;    {        u2 start_pc;        u2 length;        u2 name_index;        u2 descriptor_index;        u2 index;    } local_variable_table[local_variable_table_length];}

LocalVariableTable_attribute结构的各项说明如下:

attribute_name_index:该项必须是指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“LocalVariableTable”。attribute_length:表示本属性的长度,不包含起始的6个字节。local_variable_table_length:给出 local_variable_table 数组的成员个数。local_variable_table[]:local_variable_table数组中的每个元素给出了某个局部变量在code数组中的作用范围,以及该局部变量在当前栈桢中的位置。每个元素都具有如下5项:start_pc,length:这两项给出了某个局部变量的作用范围,即code数组的 [start_pc, start_pc + length) 之间,超出了该作用域局部变量就可以回收复用了。name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示该局部变量的名称。descriptor_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示一个字段描述符(field descriptor),用来说明该局部变量在源程序中的类型。index:给出该局部变量在当前栈桢的局部变量表中的位置(索引)。如果该局部变量的类型为long或double,则它会占用 index 和 index + 1 两个位置(因为这两个类型占两个slot)。2.7 InnerClasses属性

InnerClasses是位于ClassFile结构的属性表中的一个可变长度的属性,表示内部类的列表,类声明中所有的内部类(包括成员内部类、静态内部类、匿名内部类和局部内部类)都会出现在该属性中。

InnerClasses属性具有如下格式:

InnerClasses_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 number_of_classes;    {        u2 inner_class_info_index;        u2 outer_class_info_index;        u2 inner_name_index;        u2 inner_class_access_flags;    } classes[number_of_classes];}

InnerClasses_attribute结构的各项说明如下:

attribute_name_index:该项必须是指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“InnerClasses”。attribute_length:表示本属性的长度,不包含起始的6个字节。number_of_classes:给出后续 classes[] 数组的元素个数。classes[]:常量池中的每一个CONSTANT_Class_info项表示的类或接口,如果不是包级成员(package member),则必须在 classes[] 数组中有一个对应元素。

如果类或接口的成员中又包含某些类或接口,那么它的常量池表(以及对应的InnerClasses属性)必须包含这些成员,即使这些类或接口没有被该class使用过。

此外,每一个嵌套类(nested class)和嵌套接口(nested interface)的常量池表中必须引用它的外围类(enclosing class)。所以放在一起,除了自身的嵌套类和嵌套接口外,这些嵌套类和嵌套接口的InnerClasses属性中还需要包含对外围类的引用。

classes[] 数组的每个元素又包含4项:

inner_class_info_index:该项必须是指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Class_info结构,表示当前嵌套类或接口。outer_class_info_index:如果当前类(classes[]数组中当前元素表示的类)不是另一个类或接口的成员(也就是说,当前类是一个顶层的类或接口)、或者是一个局部类或匿名类,那么out_class_info_index项的值为0。而在其他情况下,out_class_info_index项必须是指向常量池的有效索引,索引处的值必须是一个CONSTANT_Class_info结构,表示当前嵌套类的外围类。inner_name_index:如果当前类为匿名类,inner_name_index项的值必须为0。否则该项必须是指向常量池的一个有效索引,索引处的值必须为一个CONSTANT_Utf8_info结构,表示当前类的简单名称,与源代码中声明该类的名称是一致的。inner_class_access_flags:该项的值为各种标志的掩码,用来表示当前类的访问权限以及其它属性。它可以在当源代码不可用时恢复类的原始信息。下表列出了可以出现在嵌套类中的访问与属性标志位。

Flag Name

Value

Interpretation

ACC_PUBLIC

0x0001

Marked or implicitly public in source.

ACC_PRIVATE

0x0002

Marked private in source.

ACC_PROTECTED

0x0004

Marked protected in source.

ACC_STATIC

0x0008

Marked or implicitly static in source.

ACC_FINAL

0x0010

Marked final in source.

ACC_INTERFACE

0x0200

Was an interface in source.

ACC_ABSTRACT

0x0400

Marked or implicitly abstract in source.

ACC_SYNTHETIC

0x1000

Declared synthetic; not present in the source code.

ACC_ANNOTATION

0x2000

Declared as an annotation type.

ACC_ENUM

0x4000

Declared as an enum type.

inner_class_access_flags项中所有未出现在上表中的标志位留作以后使用。在生成的class文件中这些bit位应该置为0并且虚拟机也会忽略他们。

2.8 Synthetic属性

Synthetic是存在于ClassFile、field_info或method_info结构的属性表中的一个固定长度的属性。凡是不存在于源代码中的类成员都必须生成对应的Synthetic属性,或者设置它的ACC_SYNTHETIC标志位。此要求唯一例外的情况在于那些由编译器生成而被认为是人工实现无关的方法,也就是表示Java语言的默认构造器的实例初始化方法、类初始化方法、Enum.values() 和 Enum.valueOf() 方法。

Synthetic属性具有如下格式:

Synthetic_attribute {    u2 attribute_name_index;     u4 attribute_length;}

Synthetic_attribute结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“Synthetic”。attribute_length:该项的值必须为0。2.9 Deprecated属性

Deprecated是存在于ClassFile、field_info或method_info的属性表中的一个固定长度的属性。一个类、接口、方法或字段可以用Deprecated属性标记,表示该类、接口、方法或字段已经被废弃了。但Deprecated属性的存在并不会改变一个类或接口的语义。

Deprecated属性具有如下格式:

Deprecated_attribute {    u2 attribute_name_index;     u4 attribute_length;}

Deprecated_attribute结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“Deprecated”。attribute_length:该项的值必须为0。2.10 EnclosingMethod属性

EnclosingMethod是ClassFile结构的属性表中一个固定长度的属性。如果一个类为局部内部类或匿名类,则它必然会有一个EnclosingMethod属性。

EnclosingMethod具有如下格式:

EnclosingMethod_attribute {    u2 attribute_name_index;     u4 attribute_length;     u2 class_index;     u2 method_index; }

EnclosingMethod_attribute结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“EnclosingMethod”。attribute_length:该项的值必须为4。class_index:指向常量池的一个有效索引,索引处的值必须为一个CONSTANT_Class_info结构,表示包含当前嵌套类的最近的外围类。method_index:如果当前类并非直接包含在某个方法或构造器中,那么method_index项的值必须为0。否则该项为指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_NameAndType_info结构,表示当前内部类所在的方法的名称和类型。2.11 Signature属性

Signature是位于ClassFile、field_info或method_info结构的属性表中一个固定长度的属性。Signature属性记录了类、接口、构造器、方法或字段上的签名信息,因为它们在声明时使用了类型变量(type variable)或参数化类型(parameterized type)。

Signature属性具有如下格式:

Signature_attribute {    u2 attribute_name_index;     u4 attribute_length;    u2 attribute_index; }

Signature_attribute结构中各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“Signature”。attribute_length:该项的值必须为2。signature_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示一个类签名(如果该Signature属性位于ClassFile结构中)、方法签名(如果该Signature属性位于method_info结构中)或一个字段签名(如果该Signature属性位于field_info结构中)。2.12 SourceDebugExtension属性

SourceDebugExteion是存在于ClassFile结构的属性表中的一个可选的变长属性,具有如下格式:

SourceDebugExtension_attribute {    u2 attribute_name_index;     u4 attribute_length;     u1 debug_extension[attribute_length]; }

SourceDebugExtension_attribute结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值是一个CONSTANT_Utf8_info结构,表示字符串“SourceDebugExtension”。attribute_length:给出该属性的长度,不包括起始的6个字节。debug_extension[]:该数组保存了扩展的调试信息,扩展调试信息对Java虚拟机理解字节码语义没有影响。2.13 LocalVariableTypeTable属性

LocalVariableTypeTable是Code属性的属性表中一个可选的、可变长度的属性,它可以帮助调试器确定一个方法在执行过程中某个局部变量的值。

与LocalVariableTable提供变量的描述符信息不同的是,LocalVariableTypeTable提供了变量的签名信息。只有在类型上使用了类型变量(type variable)或参数化类型(parameterized type)的变量才会有意义,这种变量会同时出现在LocalVariableTable和LocalVariableTypeTable中。而其他的变量只会出现在LocalVariableTable中。

LocalVariableTypeTable具有如下格式:

LocalVariableTypeTable_attribute {    u2 attribute_name_index;     u4 attribute_length;     u2 local_variable_type_table_length;     {        u2 start_pc;         u2 length;         u2 name_index;         u2 signature_index;         u2 index;     } local_variable_type_table[local_variable_type_table_length];}

LocalVariableTypeTable的结构与LocalVariableTable大致相同,除了signature_index项是指向常量池的有效索引,表示变量的签名信息而非描述符信息。LocalVariableTypeTable_attribute结构的其他项在此不再赘述。

2.14 RuntimeVisibleAnnotations属性

RuntimeVisibleAnnotations是位于ClassFile、field_info或method_info结构的属性表中一个可变长度的属性。RuntimeVisibleAnnotations属性记录了类、字段或方法在声明时所用的运行时可见的注解。Java虚拟机必须保证这些注解在运行时可以被相应的反射接口访问到。

RuntimeVisibleAnnotations属性具有如下格式:

RuntimeVisibleAnnotations_attribute {    u2 attribute_name_index;     u4 attribute_length;     u2 num_annotations;    annotation annotations[num_annotations];}

RuntimeVisibleAnnotations_attribue结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须为一个CONSTANT_Utf8_info结构,表示字符串“RuntimeVisibleAnnotations”。attribute_length:给出该属性的长度,不包括起始的6个字节。num_annotations:给出该属性所表示的运行时可见的注解的个数。annotations[]:annotations数组中每一个元素都表示一个运行时可见的注解。annotation结构具有如下格式:

annotation {    u2 type_index;     u2 num_element_value_pairs;     {        u2 element_name_index;         element_value value;     } element_value_pairs[num_element_value_pairs];}

annotation结构的各项说明如下:

type_index:指向常量池的一个有效索引,索引处的值必须为一个CONSTANT_Utf8_info结构,表示一个字段描述符的字符串。字段描述符给出该annotation结构所表示的注解的类型。num_element_value_pairs:给出注解的element-value对的数量。element_value_pairs[]:element_value_pairs数组的每一项都表示(该annotation结构所表示的)注解的一个element-value对。而每一项又包含了两个子项:element_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,给出了注解中该元素的名称。value:给出注解中该元素的值。2.14.1 element_value结构

element_value结构是一个可区分的联合体(discriminated union),用来表示element-value对的值。它具有如下格式:

element_value {    u1 tag;    union {        u2 const_value_index;        {   u2 type_name_index;            u2 const_name_index;        } enum_const_value;        u2 class_info_index;        annotation annotation_value;        {   u2            num_values;            element_value values[num_values];        } array_value;    } value;}

tag项用单个ASCII字符来表示element-value对的值的类型,这决定了将使用“value”联合体的哪一个元素。下表中给出了tag项的有效字符、每个字符所表示的数据类型、及其对应的“value”联合体的元素。该表的第4列将会在后续描述联合体的每一个元素时用到。

tag item

Type

value Item

Constant Type

B

byte

const_value_index

CONSTANT_Integer

C

char

const_value_index

CONSTANT_Integer

D

double

const_value_index

CONSTANT_Double

F

float

const_value_index

CONSTANT_Float

I

int

const_value_index

CONSTANT_Integer

J

long

const_value_index

CONSTANT_Long

S

short

const_value_index

CONSTANT_Integer

Z

boolean

const_value_index

CONSTANT_Integer

s

String

const_value_index

CONSTANT_Utf8

e

Enum type

enum_const_value

Not applicable

c

Class

class_info_index

Not applicable

@

Annotation type

annotation_value

Not applicable

[

Array type

array_value

Not applicable

value项表示实际的element-value对的值,该项是一个联合体,包含的元素列举如下:

const_value_index:该项表示一个基本数据类型的常量值或一个字符串字面量。const_value_index是指向常量池的一个有效索引,索引处的值必须与tag项所允许的类型相匹配,如上表所示。enum_const_value:该项表示用一个枚举常量作为element-value对的值。enum_const_value项又包含两个子项:type_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示一个字段描述符的字符串。描述符给出了枚举常量所属的类型。const_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示枚举常量的简单名(simple name)。class_info_index:该项表示用一个类字面量(class literal)作为element-value对的值。class_info_index项是指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示一个返回值描述符(return descriptor),给出类字面量对应的类型。annotation_value:该项表示用一个“嵌套的”注解作为该element-value对的值。array_value:该项表示用一个数组作为该element-value对的值。array_value项又包含两个子项:num_values:给出该element_value结构中元素的个数。values[]:给出该element_value结构表示的数组中各个元素的值。2.15 RuntimeInvisibleAnnotations属性

RuntimeInvisibleAnnotations是ClassFile、field_info或method_info结构中一个可变长度的属性。RuntimeInvisibleAnnotations属性记录了对应的类、字段和方法在声明时的运行时不可见的注解。

RuntimeInvisibleAnnotations属性与RuntimeVisibleAnnotations是类似的,除了RuntimeInvisibleAnnotations属性所表示的注解在运行时一定不能被反射接口访问到,Java虚拟机也会忽略该属性(唯一例外之处在于,Java虚拟机可能通过特定的实现方法来保留这些注解,例如命令行标记参数)。

RuntimeInvisibleAnnotations属性具有如下格式:

RuntimeInvisibleAnnotations_attribute {    u2 attribute_name_index;     u4 attribute_length;     u2 num_annotations;     annotation annotations[num_annotations];}

RuntimeInvisibleAnnotations_attribute中的各项说明如下:

attribute_name_index:指向常量池的有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“RuntimeInvisibleAnnotations”。attribute_length:给出该属性的长度,不包括起始的6个字节。num_annotations:给出该属性表示的运行时不可见的属性的个数。annotations[]:该数组中的每一项都给出了声明时的一个运行时不可见的注解。annotation结构可以参考RuntimeVisibleAnnotations属性的说明。2.16 RuntimeVisibleParameterAnnotations属性

RuntimeVisibleParameterAnnotations是method_info结构的属性表中一个可变长度的属性。RuntimeVisibleParameterAnnotations属性记录了对应方法的形参(formal parameters)在声明所用的运行时可见的注解。Java虚拟机必须保证这些注解能够被对应的反射接口访问并返回。

RuntimeVisibleParameterAnnotations属性具有如下格式:

RuntimeVisibleParameterAnnotations_attribute {    u2 attribute_name_index;     u4 attribute_length;     u1 num_parameters;     {        u2 num_annotations;         annotation annotations[num_annotations];    } parameter_annotations[num_parameters];}

RuntimeVisibleParameterAnnotations_attribute结构各项的说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“RuntimeVisibleParameterAnnotations”。attribute_length:给出该属性的长度,不包括起始的6个字节。num_parameters:该项的值给出了属性所属方法的形参的个数。事实上方法的形参个数可以从方法的描述符中提取出来,所以num_parameters是重复的。parameter_annotations:该数组中的每一项表示单个参数声明时的所有运行时可见的注解。数组中第 i 项对应于方法描述符中的第 i 个参数。parameter_annotations 数组中的每一项又包含了两个子项:num_annotations:表示该参数上声明的运行时可见注解的个数。annotations[]:数组中的每一项表示参数上声明的一个运行时可见的注解。2.17 RuntimeInvisibleParameterAnnotations属性

RuntimeInvisibleParameterAnnotations是位于method_info结构的属性表中一个可变长度的属性,它记录了对应方法的形参上声明的运行时不可见的注解信息。

RuntimeInvisibleParameterAnnotations属性与RuntimeVisibleParameterAnnotations属性类似,除了RuntimeInvisibleParameterAnnotations属性表示的注解在运行时一定不能被反射接口访问到,Java虚拟机也会忽略该属性(唯一例外之处在于,Java虚拟机可能通过特定的实现方式来保留这些注解,例如命令行标记参数)。

RuntimeInvisibleParameterAnnotations属性具有如下格式:

RuntimeInvisibleParameterAnnotations_attribute {    u2 attribute_name_index;    u4 attribute_length;    u1 num_parameters;    {          u2 num_annotations;        annotation annotations[num_annotations];    } parameter_annotations[num_parameters];}

除了attribute_name_index(指向常量池的有效索引,索引处的值类型为CONSTANT_Utf8_info,表示字符串“RuntimeInvisibleParameterAnnotations”)之外,RuntimeInvisibleParameterAnnotations_attribute结构中的各项与RuntimeVisibleParameterAnnotations_attribute结构基本一致,在此不再赘述。

2.18 AnnotationDefault属性

AnnotationDefault是位于特定method_info结构的属性表中的一个可变长度的属性,也就是表示注解类型的元素的method_info结构。AnnotationDefault属性记录了该method_info表示的注解元素的默认值。Java虚拟机必须保证这些默认值是能够被对应的反射API访问到的。

AnnotationDefault属性具有如下格式:

AnnotationDefault_attribute {    u2 attribute_name_index;     u4 attribute_length;    element_value default_value; }

AnnotationDefault_attribute属性的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“AnnotationDefault ”。attribute_length:给出字符串的长度,不包含起始的6个字节。default_value:表示注解元素的默认值,element_value的结构在介绍RuntimeVisibleAnnotations属性时已经有过详细的说明,凡是能出现在注解元素中的值的类型也能出现在元素的默认值中。2.19 StackMapTable属性

StackMapTable是Code属性的属性表中一个可变长度的属性,用于类型检查校验阶段。

在class文件50.0及以上版本中,如果一个方法的Code属性没有StackMapTable属性,那么该Code属性就有了一个隐式的栈映射表属性。这个隐式的栈映射表相当于 number_of_entries 项为0的StackMapTable属性。

StackMapTable属性具有如下格式:

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

StackMapTable_attribute结构的各项说明如下:

attribute_name_index:指向常量池的有效索引,索引处的值必须为一个CONSTANT_Utf8_info结构,表示字符串“StackMapTable”。attribute_length:表示该属性的长度,不包括起始的6个字节。number_of_entries:给出 entries[] 数组中 stack_map_frame(栈映射帧)的个数。entries[]:数组中的每一项都描述了该方法中的一个栈映射帧。entries[] 数组中的栈映射帧的顺序是有意义的。一个栈映射帧(显式或隐式的)指定了它所应用的字节码偏移量、以及该偏移量处(方法执行到此处时)的局部变量表和操作数栈中槽的验证类型(verification type)。

entries表中的各个映射帧从语义上来说会依赖它的前一个帧。一个方法的第一个栈映射帧是隐式的,是由类型检查器从方法描述符中计算出来的,因此位于 entries[0] 的stack_map_frame结构其实是描述了方法的第二个栈映射帧。

一个栈映射帧所生效的字节码偏移量的计算方式为:取出该帧中的 offset_delta 项的值(显式或隐式的),然后对前一帧的字节码偏移量加上 offset_delta + 1,除非前一帧为该方法的初始帧。在那种情况下(前一帧为初始帧),当前帧映射帧生效的字节码偏移量就是该帧中 offset_delta 项的值。

我们说字节码中的一条指令具有对应的栈映射帧,如果该指令位于Code属性的code数组的偏移量 i 处,并且该Code属性具有一个StackMapTable属性,它的entries数组包含一个应用到字节码偏移量 i 处的栈映射帧。

一个验证类型(verification type)用来指定一个或两个location处的数据类型,而一个location可以是单个局部变量或单个操作数栈的slot。一个验证类型是由可区分的联合体 verification_type_info 表示的,该联合体包含一字节个tag,表示用联合体的哪一项,后跟零个或多个字节,给出验证类型的更多信息。verification_type_info具有如下结构:

union verification_type_info {    Top_variable_info;     Integer_variable_info;     Float_variable_info;     Long_variable_info;     Double_variable_info;     Null_variable_info;     UninitializedThis_variable_info;     Object_variable_info;     Uninitialized_variable_info; }

用来指定局部变量数组或操作数栈中一个location的验证类型是由 verification_type_info 联合体的如下元素表示的:

Top_variable_info项说明当前局部变量的验证类型为top:

Top_variable_info {    u1 tag = ITEM_Top; /* 0 */}
Integer_variable_info项说明当前位置的验证类型为int:
Integer_variable_info {    u1 tag = ITEM_Integer; /* 1 */}
Float_variable_info项说明当前位置的验证类型为float:
Float_variable_info {    u1 tag = ITEM_Float; /* 2 */}
Null_variable_info项说明当前位置的验证类型为null:
Null_variable_info {    u1 tag = ITEM_Null; /* 5 */}
UninitializedThis_variable_info项说明当前位置的验证类型为UninitializedThis:
UninitializedThis_variable_info {    u1 tag = ITEM_UninitializedThis; /* 6 */}
Object_variable_info项说明当前位置的检查类型为某个类,该类是由cpool_index元素指向的常量池表中CONSTANT_Class_info结构指定。
Object_variable_info {    u1 tag = ITEM_Object; /* 7 */    u2 cpool_index;}
Uninitialized_variable_info项说明当前位置的 验证类型为 unitialized(Offset)。Offset 元素给出本StackMapTable属性所属的Code属性中code数组的偏移量,该偏移量处会通过 new 指令创建存储到当前位置的对象。
Uninitialized_variable_info {    u1 tag = ITEM_Uninitialized; /* 8 */    u2 offset;}

指定局部变量数组或操作数栈中两个位置的验证类型是由 verification_type_info 联合体中的如下两个元素表示的:

Long_variable_info项说明当前位置的验证类型为long:

Long_variable_info {    u1 tag = ITEM_Long; /* 4 */}
Double_variable_info项说明当前位置的验证类型为double:
Double_variable_info {    u1 tag = ITEM_Double; /* 3 */}

一个栈映射帧是由一个可区分的联合体 stack_map_frame表示的。该联合体包含一个一字节的tag,表示使用联合体中的哪个元素,后跟零个或多个字节,给出该tag的更多信息。stack_map_frame结构具有如下格式:

union stack_map_frame {    same_frame;     same_locals_1_stack_item_frame;     same_locals_1_stack_item_frame_extended;     chop_frame;     same_frame_extended;     append_frame;     full_frame; }

tag给出了栈映射帧的帧类型:

tag值在 [0-63] 之间表示same_frame帧类型。该类型表示当前帧具有和前一个帧相同的局部变量表以及空的操作数栈。当前帧的offset_delta为tag元素的值,也就是 frame_type项的值。

same_frame {    u1 frame_type = SAME; /* 0-63 */}
tag值在 [64-127] 之间表示same_locals_1_stack_item_frame帧类型。该类型表示当前帧具有和前一个帧相同的局部变量表而操作数栈中只有一元素。当前帧的offset_delta值可以由 frame_type - 64 计算得到。操作数栈中唯一元素的验证类型由帧类型后的多个字节给出。
same_locals_1_stack_item_frame {    u1 frame_type = SAME_LOCALS_1_STACK_ITEM; /* 64-127 */    verification_type_info stack[1];}
tag在 [128-246] 之间的值留待将来使用。tag值为247表示same_locals_1_stack_item_frame_extended帧类型。该类型表示当前帧具有和前一个帧相同的局部变量表而操作数栈中只有一个元素。与same_locals_1_stack_item_frame帧类型不同的是,same_locals_1_stack_item_frame_extended显式给出了offset_delta项的值。操作数栈中唯一元素的验证类型由offset_delta后的多个字节给出。
same_locals_1_stack_item_frame_extended {    u1 frame_type = SAME_LOCALS_1_STACK_ITEM_EXTENDED; /* 247 */    u2 offset_delta;    verification_type_info stack[1];}
tag值在 [248-250] 之间表示chop_frame帧类型。该类型表示当前帧除了最后 k 个局部变量缺失外,与前一个帧具有相同的局部变量表,并且操作数栈为空。k 值可以由 251 - frame_type 计算得到。该帧类型的offset_delta值是显式给出的。shop_frame帧出现的原因是当前方法中的一个代码块执行结束,这个代码块中声明的局部变量就可以回收了,所以相比前一个栈映射帧缺少了这些局部变量。
chop_frame {    u1 frame_type = CHOP; /* 248-250 */    u2 offset_delta;}
tag值为251表示same_frame_extended帧类型。该类型说明当前帧具有和前一个帧相同的局部变量表而操作数栈为空。与same_frame帧类型不同的是,same_frame_extended帧中的offset_delta值是显式给出的。
same_frame_extended {    u1 frame_type = SAME_FRAME_EXTENDED; /* 251 */    u2 offset_delta;}
tag值在 [252-254] 之间表示append_frame帧类型。该类型说明当前帧除了新定义的 k 个布局变量外,具有和前一个帧相同的局部变量,并且操作数栈为空。k 值可以由 fram_type - 251计算得到。append_frame帧的offset_delta值是显式给出的。
append_frame {    u1 frame_type = APPEND; /* 252-254 */    u2 offset_delta;    verification_type_info locals[frame_type - 251];}

append_frame中 locals[0] 表示第一个新定义的局部变量,以此类推。注意long和double类型的变量需要占用两个位置。

tag值为255表示full_frame帧类型。该类型给出了当前帧的局部变量表和操作数栈中全量元素的验证类型。full_frame帧的offset_delta值是显式给出的。

full_frame {    u1 frame_type = FULL_FRAME; /* 255 */    u2 offset_delta;    u2 number_of_locals;    verification_type_info locals[number_of_locals];    u2 number_of_stack_items;    verification_type_info stack[number_of_stack_items];}

locals[]数组中的第0个元素表示第0个局部变量的验证类型。同样地,stack[]数组中的第0个元素表示操作数栈中最底层操作数的验证类型,stack[]数组中的后续元素依次表示向栈顶处的操作数的验证类型。

其实StackMapTable属性中所有的映射帧都可以用full_frame类型表示,但为了压缩空间、减少生成的字节码文件的大小,引入了其他类型帧类型。将当前帧与前一帧做比较,只记录差异和变化的部分。由于一个方法中代码的执行宏观上是顺序的,因此相邻映射帧之间相当一部分的局部变量和操作数是相同的。

2.20 BootstrapMethods、RuntimeVisibleTypeAnnotations与RuntimeInvisibleTypeAnnotations

这三个属性的结构比较复杂,并且它们所支持的语言特性在平时很少用到,所以本文中不做深入剖析。

2.21 MethodParameters属性

MethodParameters是method_info结构的属性表中一个可变长度的属性。一个MethodParameters属性记录了方法的形参的相关信息,例如参数的实际名称。

MethodParameters属性具有如下格式:

MethodParameters_attribute {    u2 attribute_name_index;    u4 attribute_length;    u1 parameters_count;    {        u2 name_index;        u2 access_flags;    } parameters[parameters_count];}

MethodParameters_attribute结构的各项说明如下:

attribute_name_index:指向常量池的一个有效索引,索引处的值必须是一个CONSTANT_Utf8_info结构,表示字符串“MethodParameters”。attribute_length:表示该属性的长度,不包含起始的6个字节。parameters_count:给出方法描述符中参数描述符的格式(方法描述符由该MethodParameters属性所属的method_info结构中的descriptor_index项指定)。parameters[]:该数组中的每一项都包含如下一对元素:name_index:name_index的值可以是0或者指向常量池的一个有效索引。如果name_index的值为0,说明编译器没有为该参数生成name。如果name_index的值不为0,常量池中该索引处的值必须为一个CONSTANT_Utf8_info结构,表示当前形参的有效的非限定名(valid unqualified name)。access_flags:该项的值可以有以下几种:

0x0010 (ACC_FINAL):表示当前形参声明为 final。

0x1000 (ACC_SYNTHETIC):说明该形参并非显式或隐式地声明在源代码中(可以参考 JLS 13.1),参数是由生成该class文件的编译器自动产生的。

0x8000 (ACC_MANDATED):说明当前形参在源代码中是隐式声明的(可以参考 JLS 13.1,也就是说语言规范中强制要求了生成该参数,因此所有针对该语言的编译器都要生成这个形参)。

parameters数组中的第 i 项对应于当前方法的描述符的第 i 个参数描述符(因为一个方法描述符中的参数个数限制为255,因此parameters_count项只需占用1个字节即可),而与源代码中方法的参数没有绝对的一一对应的关系。

parameters数组中的第 i 项可能会也可能不会,对应于当前方法的Signature属性(如果存在的话)的第 i 个类型,或者对应于当前方法的参数注解中的第 i 个注解。

3. 实例详解

以上对Java虚拟机规范中预定义属性的结构进行了详细的说明,下面将结合实例借助jclasslib工具直观展示生成的class文件中这些属性的结构以及各个元素的值,以加深理解。

3.1 ConstantValue属性

从对ConstantValue属性的描述中可知,只有final修饰的、带有常量表达式的字段才会生成ConstantValue属性。初始化表达式的值在编译期计算出来并放置到常量池中,然后把在常量池中的索引写入ConstantValue属性的constantvalue_index元素中。

代码示例:

场景

源代码

生成的字节码

基本类型字段带final修饰

public class AppMain {    private static final int _1M = 1024 * 1024;         public static void main(String[] args) {        System.out.println(_1M);    }}

说明字段 _1M 上生成了ConstantValue属性,属性的 attribute_name_index项指向常量池索引9,表示字符串“ConstantValue”。

attribute_length项的值为2。constantvlaue_index项指向常量池索引4,索引处的值为1048576。

字段不带final修饰

public class AppMain {    private static int _1M = 1024 * 1024;    public static void main(String[] args) {        System.out.println(_1M);    }}

没有生成ConstantValue属性。

final字段延迟初始化

public class AppMain {    private final int _1M;    public AppMain() {        _1M = 1024 * 1024;    }    public static void main(String[] args) {        AppMain appMain = new AppMain();        System.out.println(appMain._1M);    }}

没有生成ConstantValue属性,但是在实例初始化方法(“<init>”方法)中有对字段 _1M 进行赋初值。

final字段是String类型

public class AppMain {    private final String s = "hello mars";    public static void main(String[] args) {        AppMain appMain = new AppMain();        System.out.println(appMain.s);    }}

字符串常量也生成了ConstantValue属性,属性的constantvalue_index项指向常量池索引2,表示字符串“hello mars”。

final字段不是基本类型

public class AppMain {    private final Integer _1M = 1024 * 1024;    public static void main(String[] args) {        AppMain appMain = new AppMain();        System.out.println(appMain._1M);    }}

没有生成ConstantValue属性,但是在实例初始化方法中有对字段 _1M 进行赋初值。

3.2 Exceptions属性

Exceptions属性表示方法声明时显式抛出的异常。

代码示例:

源代码

生成的字节码

public class AppMain {    public static void main(String[] args)            throws IllegalArgumentException, IOException, AssertionError {        System.out.println("hello mars");    }}

main方法上生成了Exceptions属性,其中IllegalArgumentException为非受检异常,IOException为受检异常,AssertionError为java.lang.Error的子类型。

3.3 SourceFile属性

SourceFile属性位于ClassFile结构的属性表中,表示该class文件是由哪个源代码文件编译生成的。默认情况下该属性是自动生成的。javac编译器的 -g 选项可以让开发者指定在class文件中生成哪些调试信息:

其中 -g:source 选项可以生成SourceFile属性。

以上述代码为例,生成的字节码为:

表示该class文件是由源码中的AppMain.java文件编译生成。

3.4 LineNumberTable属性

行号表属性用来确定源码中的行号与方法生成的指令序列之间的对应关系,是一个可选属性,可以辅助代码调试。当希望压缩生成的字节码文件的大小,可以在编译时选择忽略该属性的生成。

代码示例:

源代码

方法指令序列

行号表

3.5 LocalVariableTable属性

局部变量表也是一个附加属性,用来帮助调试器确定方法在执行过程中各个局部变量的值。

源代码

方法指令序列

局部变量表

上图是main方法的LocalVariables属性,从中可知:

方法入参args位于局部变量表的slot 0处,作用范围为code数组的 [0, 38),即整个方法体。

按照变量的声明顺序,变量a位于slot 1处,作用范围为[3, 3 + 35)。

变量b位于slot 2处,作用范围为[6, 6 + 32)。

变量i位于slot 3处,作用范围为[8, 8 + 19)。

变量d的作用范围为[32, 32 + 6),由于变量d声明的时候变量i已经退出作用范围,因此复用了slot 3。又因为变量d的类型为long,所以占用了slot 3和slot 4两个位置。

变量c位于slot 5处,作用范围为[37, 37 + 1)。

3.6 InnerClasses和EnclosingMethod属性

这两个属性用来表示内部类与外部类之间的映射关系。下面将分别分析4种内部类中这两个属性的结构。

源代码

public class AppMain {     // 匿名类    private Function anonymousFunc1 = new Function() {        @Override        public Object apply(Object o) {            return "hello";        }    };     public static void main(String[] args) {        Function anonymousFunc2 = new Function() {            @Override            public Object apply(Object o) {                return "world";            }        };         class LocalCls implements Serializable {        }    }     // 成员内部类    private class MemberCls {    }     // 静态内部类    private static class StaticCls {    }}

AppMain类的字节码

AppMain类生成的字节码具有InnerClasses属性,其中包含5个内部类,分别介绍如下:

第0项表示类AppMain$StaticCls,即为AppMain的静态内部类StaticCls。

第1项表示类AppMain$MemberCls,即为AppMain的成员内部类MemberCls。

第2项表示类AppMain$1LocalCls,即为main方法中声明的局部内部类LocalCls。由于它是个局部类,因此该项的outer_class_info_index的值为0,指向常量池中一个无效的索引。

第3项表示类AppMain$2,即为main方法中变量anonymousFunc2所属的类型。由于它是个匿名类,因此该项的outer_class_info_index和inner_name_index的值为0,指向常量池中一个无效的索引。

第4项表示类AppMain$1,即为字段anonymousFunc1所属的类型。由于它是个匿名类,因此该项的outer_class_info_index和inner_name_index的值为0。

MemberCls类的字节码

MemberCls是AppMain中的一个成员内部类,它也有一个InnerClasses属性,其中包含的一个元素与AppMain类的InnerClasses属性中的第1个元素相同。

StaticCls类的字节码

StaticCls是AppMain中的一个静态内部类。

字段anonymousFunc1所属类的字节码

字段anonymousFunc1所属的类型是一个匿名内部类,被编译器编译为AppMain$1。它具有一个InnerClasses属性,其中包含的一个元素与AppMain类的InnerClasses属性中的第4个元素相同

并且它还具有一个EnclosingMethod属性,属性的class_index项指向外围类AppMain,由于它不位于任何方法内部,因此method_index项的值为0。

根据定义,如果一个类为匿名类或局部类,那么它必须有一个EnclosingMethod属性。

变量anonymousFunc2所属类的字节码

变量anonymousFunc2所属的类型是一个匿名内部类,被编译为AppMain$2。它具有一个InnerClasses属性,其中包含的一个元素与AppMain类的InnerClasses属性中的第3个元素相同。

并且它还具有一个EnclosingMethod属性,属性的class_index项指向外围类AppMain,method_index项表示所在的方法为main()。

LocalCls类的字节码

LocalCls类是一个局部内部类,它同时具有InnerClasses和EnclosingMethod两个属性。

3.7 Signature属性

Signature属性记录了类、方法和字段上的类型擦除前的泛型信息,以便运行时能通过反射API访问到它们。

代码示例:

源代码

abstract class TFunction<T, R> implements Function<T, R> {    private T var1;    private List<R> var2;     public abstract <E> E apply(T t, R r);     public abstract <E extends String & CharSequence> Set<E> apply2(List<T> listT, List<R> listR);}

类上的签名

从类TFunction的声明可知,它是一个参数化类型,带有两个类型变量T和R。

生成的字节码中,ClassFile结构的属性表中Signature属性为:

签名字符串复制如下:

“<T:Ljava/lang/Object;R:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/function/Function<TT;TR;>;”

从该签名字符串可知,类型变量的T和R在声明时没有指定上边界(upper bounds),因此上边界默认为java.lang.Object;

TFunction类在声明时没有指定父类型,因此默认的父类为java.lang.Object;

TFunction类实现了接口java.lang.Function,并且使用了TFunction声明的两个类型变量T和R。

字段上的签名

字段var1的Signature属性为:

字段var2的Signature属性为:

说明var1的类型为类型变量T,而var2的类型为参数化类型List<R>。

方法上的签名

方法apply()和apply2()上都声明了类型变量E,两者的Signature属性分别为:

从以上两个方法签名可知,方法apply()上的类型变量E在声明时没有指定上边界,因此默认上边界为java.lang.Object;而apply2()上声明的类型变量具有java.lang.String和java.lang.CharSequence两个上边界。

3.8 LocalVariableTypeTable属性

用来记录方法中入参和局部变量使用的泛型信息,只有使用类型变量或参数化类型的变量才会出现在该属性中。这种变量会同时出现在LocalVariableTable和LocalVariableTypeTable中,而其他的变量只会出现在LocalVariableTable中。

代码示例:

源代码

class TFunction<T, R> implements Function<T, R> {     @Override    public R apply(T t) {        R r = (R) t;        String s = "hello mars";        System.out.println(s);        List<T> l = new ArrayList<>();        return r;    }}

LocalVariableTable属性

apply()方法中的入参和局部变量按照声明顺序都出现在了LocalVariableTable属性中。并且由于该方法是非static的,所以默认第0个slot为this指针(当前执行该方法的对象)。

LocalVariableTypeTable属性

入参t和变量r分别为类型变量T和R,而变量 l 的为参数化类型List<T>,因此它们存在于LocalVariableTypeTable属性中。

注意this指针也出现在了该属性中。

3.9 RuntimeVisibleVisibleAnnotations和RuntimeInvisibleAnnotations属性

这两个属性分别用来表示类、字段、构造器和方法上声明的运行时可见和不可见的注解。其中运行时可见的注解能够在运行时被反射接口访问到,并基于注解和其中的元素值执行用户自定义的程序。

代码示例:

源代码

// 用来验证嵌套在其他注解中的枚举类型enum AccessLevel {    PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE;} // 用来验证嵌套在其他注解中的注解类型@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@interface CsvFileSource {    String[] resources();    String encoding() default "UTF-8";    String lineSeparator() default "\n";    char delimiter() default ',';    int numLinesToSkip() default 0;} // 该注解的使用只会存在于Java源码中,编译时被编译器忽略@Retention(RetentionPolicy.SOURCE)@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})@interface SourceVisible {} // 该注解的使用会被编译进class文件,但不必在运行时加载到VM中,也不能被反射接口读取@Retention(RetentionPolicy.CLASS)@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})@interface ClassVisible {    int intVal() default 0;    long longVal() default 0L;    String strVal() default "";    AccessLevel enumVal() default AccessLevel.PUBLIC;    Class<?> clzVal() default AbstractList.class;    CsvFileSource annotVal() default @CsvFileSource(resources = {"1.csv", "2.csv"});    short[] arrVal() default {};} // 该注解的使用会被编译器记录到class文件中,并在运行时加载到VM中,可以被反射接口读取@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})@interface RuntimeVisible {    int intVal() default 0;    long longVal() default 0L;    String strVal() default "";    AccessLevel enumVal() default AccessLevel.PUBLIC;    Class<?> clzVal() default AbstractList.class;    CsvFileSource annotVal() default @CsvFileSource(resources = {"1.csv", "2.csv"});    short[] arrVal() default {};} @SourceVisible@ClassVisible(strVal = "hello world", clzVal = LinkedList.class, intVal = 6, arrVal = {1, 2, 3, 4, 5}, enumVal = AccessLevel.PACKAGE)@RuntimeVisible(strVal = "hello mars", annotVal = @CsvFileSource(resources = {"3.csv"}, delimiter = ':', numLinesToSkip = 10))public class AppMain {    public static void main(String[] args) throws Throwable {        Class<AppMain> clz = AppMain.class;        Annotation[] annotations = clz.getDeclaredAnnotations();        for (Annotation annotation : annotations) {            System.out.println("通过反射接口可以访问到注解类型为: " + annotation.annotationType().getName());            if (annotation instanceof RuntimeVisible) {                RuntimeVisible runtimeVisible = (RuntimeVisible) annotation;                System.out.println("它的strVal元素的值为: " + runtimeVisible.strVal());            }        }    }}

从代码中可知,SourceVisible、ClassVisible和RuntimeVisibe分别为源码可见、Class文件可见和运行时可见的注解,并在AppMain类声明时使用了这三个注解。

RuntimeVisibleAnnotations属性

可以看到AppMain类生成的class文件的属性表中具有一个RuntimeVisibleAnnotations属性,其中包含一个注解,注解的类型为“com.xiaotaotao.test.utils.RuntimeVisible”。

其中包含两个element-value对,第一个键值对的类型tag为“s”,表示字符串类型。key为“strVal”,value为指向类型为CONSTANT_Utf8_info结构的常量池的索引,表示字符串“hello mars”。

第二个键值对的类型tag为“@”,表示注解类型。key为“annotVal”,value为CsvFileSource注解实例的结构,其中又包含了三个element-value对,分别表示CsvFileSource注解的resources、delimiter和numLinesToSkip三个元素的值,在此不再展开描述。

需要注意的一点是,RuntimeVisible注解在声明时各个元素都带有默认值,在使用时不需要指定所有元素的值,因此在生成的字节码中只记录了显式指定值的元素的element-value对,而没有显式指定值的元素没有记录在字节码中。在运行时反射接口可以通过读取注解类的class文件中对应元素的默认值来填充,可以参考 AnnotationDefault 注解的说明和实例讲解。

RuntimeInvisibleAnnotations属性

可以看到AppMain类生成的class文件的属性表中具有一个RuntimeVisibleAnnotations属性,其中包含一个注解,类型为“com.xiaotaotao.test.utils.ClassVisible”。

其中包含五个element-value对,分别表示ClassVisible注解中strVal、clzVal、intVal、arrVal和enumVal五个元素的值。与上述分析方式相同,在此不再展开。

与RuntimeVisibleAnnotations属性相同,没有显式指定值的元素没有出现在字节码中,在运行时反射接口可以通过读取注解类的class文件中对应元素的默认值来获取。

SourceVisible注解

SourceVisible注解没有出现在AppMain类的属性表中,因此它的使用被编译器忽略了。但这并不代表SourceVisible注解没有生成class文件,事实上它的class文件形式如下:

运行结果

程序的输出结果如下:

从输出结果可以看到,通过反射只取到了AppMain类上使用的运行时可见注解RuntimeVisible,而访问不到运行时不可见注解ClassVisible。并且通过反射能渠道RuntimeVisible注解上元素strVal的值。

有读者可能会有疑问,既然RetentionPolicy为SOURCE的注解的使用无法编译进字节码中,似乎没有任何作用,为什么还保留它呢?事实上,SOURCE类型的RetentionPolicy为编译器提供了辅助支持。在JDK 1.6中实现了JSR-269规范:Pluggable Annotations Processing API(插件式注解处理API),提供了一组插件式注解处理器的标准API用来在编译期对注解进行处理。我们可以把它看作是一组编译器的插件,在这组插件中可以读取、修改、添加抽象语法树的任意节点。

Lombok就是JSR-269特性的典型应用,相信大家在平时的编码工作中也经常用到,的确能减少编码量、消除冗余、使代码清晰简洁。

3.10 RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameterAnnotations属性

这两个属性与RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性相似,只是他们用来表示作用在方法参数上的注解,并且RuntimeVisibleParameterAnnotations是运行时可见的,可以通过反射接口访问到。

代码示例:

源代码

// @SourceVisible、@ClassVisible和@RuntimeVisible复用上个示例的定义public class AppMain {     private void sayHello(@SourceVisible @ClassVisible(intVal = 10, clzVal = Set.class) int param1,                          @RuntimeVisible(enumVal = AccessLevel.MODULE) String param2) {        System.out.println("hello " + param1 + " " + param2);    }     public static void main(String[] args) throws Throwable {        Class<AppMain> clz = AppMain.class;        Method method = clz.getDeclaredMethod("sayHello", int.class, String.class);        Annotation[][] methodParamAnnotations = method.getParameterAnnotations();        for (int i = 0; i < methodParamAnnotations.length; i++) {            Annotation[] paramAnnotations = methodParamAnnotations[i];            System.out.println("参数" + (i + 1) + "上使用了" + paramAnnotations.length + "个运行时可见注解");            if (paramAnnotations.length == 0) {                System.out.println();                continue;            }             for (int j = 0; j < paramAnnotations.length; j++) {                Annotation annotation = paramAnnotations[j];                System.out.println("第" + (j + 1) + "个注解类型:" + annotation.annotationType());            }        }    }}

RuntimeVisibleParameterAnnotations属性

在生成的字节码中,sayHello方法的属性表中出现了RuntimeVisibileParameterAnnotations属性,它的parameter_annotations[]项的长度为2,因为方法入参的个数为2。

其中第0个元素对应参数param1,由于它没有使用运行时可见注解,因此该项为空。

第1个元素对应参数param2,它使用了一个运行时可见注解,因此该项具有一个类型为RuntimeVisible的注解元素。

RuntimeInvisibleParameterAnnotations属性

sayHello方法的RuntimeInvisibleParameterAnnotations属性与其RuntimeVisibleParameterAnnotations刚好相反。parameter_annotations[]项的长度同样为2,而第1项有1个元素,第2项为空。

运行结果

程序的输出结果如下所示:

说明方法入参上的运行时不可见注解无法通过反射访问到。

3.11 AnnotationDefault属性

AnnotationDefault属性用来表示注解声明时使用default关键字定义的元素的默认值。

以上述@ClassVisible注解为例,它编译生成的class文件如下:

注解的access flag为ACC_INTERFACE、ACC_ABSTRACT和ACC_ANNOTATION,默认继承自java.lang.Object类,并且实现java.lang.annotation.Annotation接口。并且注解声明时的元素会被编译成method_info结构。

@ClassVisible注解中的所有method_info结构都带有AnnotationDefault结构,因为它们对应的元素在声明时都使用default关键字定义了默认值。

3.12 StackMapTable属性

StackMapTable属性可以记录对应方法在运行过程中,各个时段的局部变量表和操作数栈的大小,以及各个slot中元素的类型,用来加快Java虚拟机对class文件的验证过程。方法起始位置处的栈映射帧可以从方法描述符中解析出来。为了节省空间,编译后的方法不会每条指令都包含一个映射帧,事实上只会在以下特定的指令上生成映射帧:对应于跳转目标处、异常处理器或紧跟在无条件跳转指令后的指令。其他的栈映射帧可以从这些映射帧简单快速地推导出来。

由于StackMapTable属性的结构复杂、映射帧的种类繁多,简单的示例无法完全覆盖,感兴趣的读者可以自己做实验验证。

3.13 MethodParameters属性

默认情况下方法参数的实际名称是不编译进字节码的,但用户可以使用javac编译器的 -parameters 选项将入参真实名称编译进对应方法的method_info结构的MethodParameters属性中,这样在运行时就可以通过反射访问到参数的real name。

java.lang.reflect.reflect.Parameter类中提供了两个方法,用来判断对应的参数上是否有真实名称并获取参数名称:

/** * Returns true if the parameter has a name according to the class * file; returns false otherwise. Whether a parameter has a name * is determined by the {@literal MethodParameters} attribute of * the method which declares the parameter. */public boolean isNamePresent();  /** * Returns the name of the parameter. If the parameter's name is * {@linkplain #isNamePresent() present}, then this method returns * the name provided by the class file. Otherwise, this method * synthesizes a name of the form argN, where N is the index of * the parameter in the descriptor of the method which declares * the parameter. */public String getName();

从注释我们得出,如果isNamePresent()方法返回true,那么getName()方法就能获取参数的真实名称;否则getName()就会合成一个参数名称“arg<N>”,其中“N”为该参数在方法描述符中出现的顺序。其中isNamePresent()判断的依据就是class文件中对应方法上有MethodParameters属性。如上所述,MethodParameters属性可以通过javac编译器的 -parameters 选项生成。

示例代码:

源代码

public class AppMain {     private void sayHello(String name, int age) {        System.out.printf("hello %s, you are %d years old", name, age);    }     public static void main(String[] args) throws Throwable {        Method method = AppMain.class.getDeclaredMethod("sayHello", String.class, int.class);        Parameter[] parameters = method.getParameters();        System.out.println("方法" + method.getName() + "有" + parameters.length + "个参数");        for (int i = 0; i < parameters.length; i++) {            System.out.println("参数" + i + "的名称为:" + parameters[i].getName());        }    }}

编译时不使用-parameters选项

编译生成的class文件中,sayHello方法如下,没有生成MethodParameters属性。

而程序执行结果为:

可见也读不到方法入参的真实名称,而是反射接口合成的名称。

编译时使用-parameters选项

编译生成的class文件中,sayHello方法上生成了MethodParameters属性。

并且程序执行的结果如下,通过反射接口读取到了方法参数的真实名称。

4. 总结

本文介绍了Java class文件中的属性,对JDK 8中预定义的23种属性的作用和结构做了详细的说明和描述,并借助jclasslib和示例程序直观展示这些属性的结构、应用场景和生效机制。

属性是class文件的一部分,而class文件是连接编译器和Java虚拟机的桥梁,它是Java源程序的另一种紧凑的、二进制的表示形式,了解了class文件对于我们后续深入编译器和JVM原理都有帮助。因此平时虽然很少用到,但了解class文件的结构还是很有必要的。

标签: #java基本属性