JVM字节码(二)
在分析完常量池后,我们来看下Java字节码的整体结构:
魔术码(Magic Number) | 4个字节 |
版本(version) | 2+2个字节 |
常量池(Constant pool) | 2+n个字节 |
访问标志(Access flags) | 2个字节 |
当前类(This Class Name) | 2个字节 |
父类(Super Class Name) | 2个字节 |
接口(Interfaces) | 2+n个字节 |
字段(Fields) | 2+n个字节 |
方法(Methods) | 2+n个字节 |
属性 (Attributes) | 2+n个字节 |
整个class文件,如果我们想用对象的形式来表达,可以编写成如下形式:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
其中constant_pool_count、interfaces_count、fields_count、methods_count和attributes_count分别代表常量池元素数量、接口数量、字段数量、方法数量、属性数量。
可以看到,在常量池之后,就是类的访问标志,我们来看下标志的类型:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 访问标志是否为public |
ACC_PRIVATE | 0x0002 | 访问标志是否为private |
ACC_PROTECTED | 0x0004 | 访问标志是否为protected |
ACC_STATIC | 0x0008 | 是否被标记为static |
ACC_FINAL | 0x0010 | 是否被标记为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_VOLATILE | 0x0040 | 是否被标记为volatile |
ACC_TRANSIENT | 0x0080 | 是否被标记为transient |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 标志这是一个抽象类 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
我们看MyTest1.class文件转换为十六进制之后的内容:
根据字节码结构,我们知道类的访问标志占两个字节,MyTest1常量池之后的两个字节是0x0021,对比之前的访问标志类型,我们发现并没有0x0021,这是因为访问标志可以多种多样,比如:public static或者public final,所以如果有多个标志位一般采用相加的形式,比如:public static是0x0009,public final是0x0011。所以MyTest1的访问标志是0x0021,拆分一下就是ACC_PUBLIC和ACC_SUPER,代表这个类是公开的,并且可以访问父类的方法。根据我们之前用java -verbose,可以看到类的访问标志确实是ACC_PUBLIC和ACC_SUPER:
…… public class com.leolin.jvm.bytecode.MyTest1 SourceFile: "MyTest1.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER ……
访问标志之后的两个字节,存储的是当前类,这里存储的索引值是0x0003,指向常量池第三个元素:com/leolin/jvm/bytecode/MyTest1。再往后推两个字节,存储的是父类,存储的索引值是0x0004,指向常量池第四个元素:java/lang/Object。
父类之后是接口,接口包含两个部分,一个是占据两个字节的接口数量(interfaces_count),还有是类所实现的接口表(interfaces)。由于MyTest1没有实现任何接口,所以父类之后的两个字节为0x0000,因此也就不存在接口表。
接口之后是字段,字段同样包含两部分,两个字节长度的字段数量(fields_count)和字段表(fields),MyTest1的字段数量为0x0001,代表只有一个字段,接着我们来看下字段信息field_info的结构:
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
第一个字段是访问标志,第二个字段是字段在常量池的索引值,第三个字段是字段的描述符在常量池的索引值,第四个是字段的属性数量,第五个字段是属性表。
我们根据field_info读取之后的数据,首先读到的是访问标志0x0002,标志名称是ACC_PRIVATE,代表这是一个私有变量,接着字段名索引值为0x0005,常量池第五个元素的值为a,字段的描述符为0x0006,代表这个字段的类型为int,到这里我们能确定这是我们在MyTest1里面定义的那个类型为int的私有变量a。接着是属性数量,这里是0x0000,由于属性数量为0,所以没有属性表。
字段之后便是方法了,方法同样包含两部分,方法数量(methods_count)和方法表(methods),这里方法数量是0x0003,有三个方法,这里我们应该很清楚为什么会有三个,除了两个我们之前生成的对字段a取值getA()和赋值setA(int a)的方法,另外一个就是编译器默认为我们生成的构造方法。下面,我们来看下方法的结构:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
和之前的字段结构一样,所以下面我们来逐个分析每个方法:
第一个方法的访问标志是0x0001,是个public方法,名字索引值是0x0007,常量池第七个元素是<init>,而描述符索引是0x0008,常量池第八个元素是()V,所以我们基本能确定这是一个构造方法,当我们再往后读两个字节,是属性值数量,这里是0x0001,构造方法有一个属性,所以我们要来看看attribute_info的结构,以便分析后面的数据。
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
第一个字段是属性名在常量池的索引,构造方法的第一个属性名在常量池的索引是0x0009,对应的是:Code,这里我们来看下Code_attribute的结构:
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 attribute_count; attribute_info attributes[attribute_count]; }
- attribute_length:表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
- max_stack:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
- max_locals:表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
- code_length:表示该方法所包含的字节码的字节数。
- code:具体字节码,即是该方法被调用时虚拟机所执行的字节码。
- 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)的指令抛出的异常会由这个表项来处理。handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常。
根据上面的Code_attribute结构,我们在0x0009(Code)之后,再读取四个字节0x00000038,十进制是56。往后的两个字节0x0002是栈能达到的最大深度,再往后的两个字节0x0001是局部变量的数目。之后的4个字节是方法所对应具体的字节码的数目,这里是0x0000000a,所以我们要再往后数十个字节:
2a b7 00 01 2a 04 b5 00 02 b1
我们节选之前反编译的内容,将构造方法的字节码贴过来:
public com.leolin.jvm.bytecode.MyTest1(); flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 4: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/leolin/jvm/bytecode/MyTest1;
我们专注Code部分从0~9这六条指令,这里我们介绍下Oracle的JVM官方文档,这里面有介绍像aload_0、invokespecial……这类助记符的作用,及助记符所对应的十六进制编码。我们先来分析上面六条指令:
- aload_0代表从本地变量表索引为0的对象推到栈顶,根据上面的局部变量表LocalVariableTable,我们知道索引为0的对象为this引用,后面还会介绍本地变量表这个属性。而aload_0的十六进制码为0x2a,正好对应上面字节指令的第一个字节。
- invokespecial代表针对父类方法、私有方法和实例构造方法的特殊调用。这里需要传入一个参数,常量池第一个元素,即:调用父类的构造方法,整体的指令是0xb7 00 01,正好与上面第二到第四个字节对应。
- 第三个指令是aload_0,之前介绍过就不介绍,对应上面第五个字节。
- iconst_1代表将常量值1推到栈顶,对应的指令是0x04,对应上面第六个字节。
- putfield表示对对象中的字段设值,这里需要传入一个参数,常量池第二个元素,即int类型的成员变量a,将栈顶的1赋值给这个变量,整体指令是0xb5 00 02,对应上面第七个到第九个字节。
- return代表从方法中返回一个void对象,对应的指令是0xb1。
至此,上面的六条指令我们都分析完毕,该方法整体的步骤就是先初始化一个对象,然后对对象中的字段设值,最后返回。
code字段结束后,就是两个字节长度的异常表长度(exception_table_length),这里是0x0000,因为编译器为我们生成的构造方法没有捕捉任何异常,所以异常表长度为0,也就没有exception_table。
之后,又是属性表长度(attribute_count)和属性表(attributes)。我们往后读两个字节,0x0002,这里有两个属性。我们先来分析第一个属性:第一个属性的索引值为0x000a,指向常量池第十个元素:行号表(LineNumberTable),现在我们来看下lineNumberTable_attribute的结构:
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]; }
之后,我们向后读取4个字节:0x0000000a,代表之后的行号表有10个字节长度的内容:
00 02 00 00 00 03 00 04 00 04
这里行号line_number_table_length占据两个字节:0x0002,代表有两行的映射,剩下的8个字节的数据,0x0000和0x0003是一对,代表偏移量为0对应到源代码第3行,0x0004和0x0004是一对,代表偏移量为4对应到源代码第四行。因为我们没有手动编写构造方法,所以编译器生成的构造方法和类名同一行,而行号4则为源代码中a=1那段,这段代码的完成是在构造方法里完成的。
至此,第一个属性就分析完毕了。我们开始第二个属性的分析,第二个属性指向常量池索引值为0x000b,即:本地变量表(LocalVariableTable),现在我们来看下LocalVariableTable_attribute的结构:
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]; }
start_pc项给出代码数组中指令开始位置的偏移量。length项给出从start_pc开始的、所有局部变量有效的、代码的长度。位于从代码数组开始偏移量start_pc +length位置处的字节只能有下列两种情况:或者为一个指令的首字节,或者为代码数组结束后的首字节。index项给出了在此方法栈帧中本地变量部分的索引,这是当方法执行时该本地变量数据所保存的位置。
向后读取四个字节0x0000000c,本地变量表我们还要往后读取12个字节。往后读取两个字节是0x0001,只有一个本地变量。开始偏移量(start_pc)是0x0000,长度(length)是0x000a,变量的名字索引是0x000c,在常量池中对应是:this,变量的描述符索引是0x000d,在常量是中对应的是:Lcom/leolin/jvm/bytecode/MyTest1;。最后一个是index,往后读取两个字节就是0x0000,this存储在索引0的位置上。至此,MyTest1的构造方法中的两个属性,我们分析完毕了。