JVM字节码(三)
接下来,就是分析MyTest1第二个方法,肯定不是int getA()就是void setA(int a)。在构造方法之后,我们读到了0x0001,这是一个访问标志为ACC_PUBLIC的方法,方法名的索引值为0x000e,类型描述符为0x000f,在常量池中分别对应getA和()I,所以我们能判定这是我们之前所定义的int getA()方法。
往后读取的是getA()的属性数量,这里是0x0001,有一个属性。于是我们往后读取两个字节,0x0009,在常量池中第九个元素是Code,于是根据Code_attribute的结构,我们还要往后读取4个字节0x0000002f,对应的十进制为47,代表这个attribute除了attribute_name_index和attribute_length之外,还占据47个字节。
这个属性中,max_stack是0x0001,代表操作数栈的最大深度为1。max_locals是0x0001,代表方法内创建的局部变量也为1。code_length为0x00000005,代表getA()方法编译后可供JVM执行的字节码占据5个字节,于是我们往后读取五个字节:2a b4 00 02 ac。这里我们看下getA()反编译后对应的字节码助记符:
public int getA(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/leolin/jvm/bytecode/MyTest1;
Code下共有3条字节码指令,这里我们依旧需要根据Oracle的JVM官方文档来分析:
- aload_0这条指令已经分析过了,对应指令0x2a,与上面5个字节码指令第一个字节码指令对应。
- getfield代表从对象中获取字段的值,对应指令0xb4。这里需要传入一个参数0x0002,在常量池中代表int类型的字段a。所以整体指令为:b4 00 02,对应上面5个字节第二个到第四个字节码指令。
- ireturn代表从方法中返回一个int类型的结果,对应指令0xac。对应上面第五个字节码指令。
往后的两个字节是异常表长度,int getA()我们没抛出任何异常,所以这里是0x0000,没有异常表。之后就是属性长度和属性表,属性长度是0x0002,有两个属性。我们先来读取第一个属性对应常量池的索引:0x000a,这里是LineNumberTable。往后读取4个字节0x00000006是attribute_length,代表距离这个属性读取完毕还有6个字节。line_number_table_length为0x0001,代表有一个映射关系,start_pc是0x0000,line_number是0x0007,至此第一个属性我们分析完毕。
接下来是第二个属性,先看常量池索引0x000b,在常量池中对应的是LocalVariableTable,根据LocalVariableTable_attribute的结构,我们往后读取四个字节0x0000000c,代表在attribute_length字段之后,还要读取12个字节这个属性才结束。我们先来读取变量表的长度local_variable_table_length,这里是0x0001,只有一个局部变量。之后start_pc为0x0000、length为0x0005、name_index为0x000c,对应常量池this、descriptor_index为0x000d,对应常量池Lcom/leolin/jvm/bytecode/MyTest1;、index为0x0000。
剩下就是第三个方法了,也就是我们之前定义的void setA(int a)。这里的访问标志依旧是0x0001,即ACC_PUBLIC。这个方法的方法名索引和描述符索引分别是0x0010和0x0011,转换成十进制为16和17,对应常量池setA和(I)V。之后是属性数量0x0001,这个方法只有一个属性,根据Code_attribute的定义,往后读取两个字节0x0009为Code,再往后读取四个字节为0x0000003e,代表还要往后读取62个字节,这个属性才结束。max_stack是0x0002,操作数栈的最大长度为2。max_locals是0x0002,方法执行期间最大局部变量数为2。code_length为0x00000006,方法经过编译后对应的字节码指令占据6个字节的长度。我们读取后面6个字节:2a 1b b5 00 02 b1,然后看下void setA(int a)对应的字节码指令:
public void setA(int); flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 11: 0 line 12: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/leolin/jvm/bytecode/MyTest1; 0 6 1 a I
这里我们依旧尝试将指令助记符翻译成上面对应的6个字节:
- 第一个aload_0对应的字节码指令0x2a,对应第一个字节码指令。
- 第二个iload_1对应字节码指令0x1b,表示从局部变量表加载索引为1的变量到栈顶,对应第二个字节码指令。
- 第三个putfield对应的字节码指令0xb5,这里接收一个参数为0x0002,为MyTest1对象的字段a,表示将栈顶的值赋予到MyTest1对象的字段a中。整体指令为0xb5 00 02。对应上面第三到第五个字节码指令。
- 第四个return对应的字节码指令是0xb1。
至此,上面方法的字节码指令对应完毕。这个方法也没有抛出异常,所以异常表数量为0x0000。接着是属性长度0x0002,这个方法有两个属性。第一个属性是0x000a,在常量池中对应LineNumberTable,往后读取四个字节0x0000000a,代表还要读取10个字节才结束这个属性,往后读取十个字节:00 02 00 00 00 0b 00 05 00 0c。
line_number_table_length为0x0002,有两个映射关系。第一对映射start_pc为0x0000,line_number为0x000b;第二对映射start_pc为0x0005,line_number为0x000c。至此,第一个属性分析完毕。
第二个属性是0x000b,对应常量池LocalVariableTable,根据LocalVariableTable_attribute的定义,我们往后读取四个字节0x00000016,这个属性还需要读取22个字节才结束。往后读取两个字节0x0002是局部变量表长度,这里有两个局部变量。第一个变量的start_pc是0x0000;length是0x0006;name_index是0x000c,对应常量池this;descriptor_index是0x000d,对应常量池Lcom/leolin/jvm/bytecode/MyTest1;;index是0x0000。第二个变量的start_pc是0x0000;length是0x0006;name_index是0x0005,对应常量池a;descriptor_index是0x0006,对应常量池I;index是0x0001。
至此,我们三个方法都分析完毕了。
class文件的方法之后,就是属性了,这里我们读取两个字节0x0001,MyTest1有一个属性。接着我们根据attribute_info往后读取两个字节0x0012,这里只想常量值第18个元素SourceFile,我们来看下SourceFile_attribute:
SourceFile_attribute { u2 attribute_name_index; u4 attribute_length; u2 sourcefile_index; }
我们先往后读取四个字节0x00000002,代表这个属性之后的长度,还有两个字节。sourcefile_index代表常量池索引,这里是0x0013,十进制为19,对应常量池元素MyTest1.java。
至此,整个MyTest1.class文件我们都分析完毕了。