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的构造方法中的两个属性,我们分析完毕了。

posted @ 2020-05-14 08:09  北洛  阅读(211)  评论(0编辑  收藏  举报