了解了方法表的结构后,我们知道方法表有自己的属性表,用来存储与当前方法相关的附加属性。属性表每一个成员的值必须是attribute
结构(如下所示:),一个方法可以有任意个与之相关的属性。
attribute_info {
u2 attribute_name_index;
u4 attibute_length;
u1 info[attibute_length]
}
JVM规范所定义的 method_info
结构中,属性表可出现的成员有:Code
,Exceptions
,Synthetic
,Signature
,Deprecated
,untimeVisibleAnnotations
, RuntimeInvisibleAnnotations
,
RuntimeVisibleParameterAnnotations
,RuntimeInvisibleParameterAnnotations
和AnnotationDefault
结构。
Code属性
这么多属性中,Code
是很常见的也是很重要的一个属性。所以,了解这个属性很有必要。
Code
属性是一个变长属性,位于 method_info
结构的属性表。一个 Code
属性只为唯一一个方法、实例类初始化方法或类初始化方法保存 Java 虚拟机指令及相关辅助信息。 所有 Java 虚拟机实现都必须能够识别 Code
属性。如果方法被声明为 native
或者abstract
类型,那么对应的 method_info
结构不能有明确的 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
attribute_name_index
项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示字符串“Code”。 - attribute_length
attribute_length
表示attribute所包含的字节数,不包含attribute_name_index
和attribute_length
字段。 - max_stack
max_stack
表示这个方法运行的任何时刻所能达到的操作栈的最大深度。 - max_locals
max_locals
项的值给出了分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量。 - code_length
code_length
项给出了当前方法的 code[]数组的字节数,即表示该方法所包含的字节码的字节数。 - code[]
code[]
数组,存放具体的字节码即时该方法被调用时,虚拟机所执行的字节码。 - exception_table_length
exception_table_length
项的值给出了exception_table[]
数组的成员个数量。 - exception_table[]
exception_table[]
这里存放的是处理异常的信息。- 每个
exception_table
表项由start_pc
,end_pc
,handler_pc
,catch_type
组成。 start_pc
和end_pc
表示在Code
数组中的从start_pc
到end_pc
处(包含start_pc
,不包含end_pc
)的指令抛出异常会由这个表项来处理。handle_pc
表示处理异常的代码的开始处。catch_type
表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type
为0时,表示处理所有异常。attributes_count
和attributes[]
自然又构成了Code
属性的附加属性。可以出现在Code
属性的属性表中的成员只能是LineNumberTable
,LocalVariableTable
,LocalVariableTypeTable
和StackMapTable
属性。
代码示例
在上一篇《方法表》里,我分析了<init>
构造方法。其中方法的Code
属性,在字节码里的展示如下所示(阴影部分):
先简单介绍一下Code
属性的16进制结构:0x0013=19,查询常量池索引19的位置得到属性的名字Code
。然后是4个字节的attribute_length
,0x00000042=66,表示存储这个Code
属性内容的字节数组的长度为66。所以往后再数66个字节结束,就得到<init>
方法完整的Code
属性信息了。也就是图上阴影部分所示。那这个Code
属性内容是什么呢?这里可以借助小工具jclasslib bytecode viewer。如下图所示:
有了jclasslib bytecode viewer这个工具,查看字节码内容就很清晰了,跟我们分析得一模一样。而且它还把属性内容里的指令翻译成助记符了,我们能清晰的看到这个<init>
方法里的java代码在编译成字节码后是什么样子的。
根据Code
属性的结构,attribute_length
之后,0x0002=2,自然代表操作栈的最大深度为2。紧接着,0x0001=1,表示局部变量表的长度为1。code_length
是0x00000010=16,也就是存放具体的字节码的Code[]
长度为16。这个Code[]
里的内容就是上面看到的,jclasslib bytecode viewer展示给我们看到的助记符了。
我们就往后数16个字节,来到了该方法的异常表处,首先是exception_table_length
异常表的长度,0x0000=0,即长度为0,就是异常表为空,没有异常表的意思。显然我们的构造方法没有声明抛出异常。所以这里的就没有exception_table[]
的信息了。
那么紧接其后的就是 attributes_count
和attributes[]
,Code
属性的属性表。0x0002=2,表示有2个属性。往后看,0x0014=20,查常量池该位置,得到LineNumberTable
字符串表示。所以第一个属性就是LineNumberTable
(行号表)。这个属性用来表示Code
数组中的字节码和Java代码行数之间的关系。这个属性可以用来在调试时候定位代码执行的行数。
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];
}
有了这个结构,我们就能找到第二个属性的名字就是,接着往后数0x0000000E=14个字节。定位到,0x0015=21,常量池查询得到,第二个属性的名字是LocalVariableTable
(局部变量表)。用来描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,非运行时必须属性。
其结构如下:
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];
}
其实通过jclasslib bytecode viewer我么可以很直观的看到LineNumberTable
和LocalVariableTable
属性的内容。如下:
总结
- 通过学习Code属性,了解到我们编写的Java代码,在字节码中是如何存储的。
- 我们在用idea进入debug模式的时候,因为有
LineNumberTable
属性,所以可以轻松地实现字节码里的指令和我们Java代码行号对应上。 LocalVariableTable
属性,如果没有这个属性,最大的影响就是当其他人引用这个方法时,所有参数名称都将会丢失,IDE就会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。