手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性

引子:

在常量池, 访问修饰符, 类和接口后面紧跟的内容是字段和方法, 这两个结构是最复杂的, 因为其里面包含有属性这个成员, 而属性又是可以嵌套的.

1. 字段

和之前的常量池一样, 因为每个class中字段的数量是不确定的, 所以字段部分的开头两个字节用于表示当前class文件中的字段的个数, 紧跟着的才是具体的字段.

先来看一下字段的结构

    Field_Info {
        u2 access_flag;
        u2 name_index;
        u2 descriptor_index;
        u2 attribute_count;
        attribute_info attributes[attribute_count];
    }
  • access_flag表示该字段的访问修饰符, 字段的访问修饰符和类的表示方式相似, 但是具体的内容不一样

    字段的访问标识

    手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第1张图片
    图1-1 FIELD-ACCESS-FLAG
  • name_index指向常量池中的name_index索引的常量项

  • descriptor_index指向常量池中的descriptor_index索引的常量项

  • attribute_count表示该字段的属性个数

  • attributes[attribute_count]表示该字段的具体的属性

注意: 这里字段的descriptor代表的字段的类型, 但是类型不是写代码的时候int, String这样整个单词的, 它是一些字符的简写, 如下:

手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第2张图片
图1-2 DESCRIPTOR

所以, 举个例子如果字段是String类型, 那么它的descriptor就是Ljava/lang/Object;如果字段是int[][], 那么它的descriptor就是[[I

对于属性的解释放到和方法属性一起

2. 方法

方法和字段一样, 也需要有一个表示方法个数的字段, 同时这个字段后面紧跟的就是具体的方法

同样, 来看一下方法的结构:

    Method_Info {
        u2 access_flag;
        u2 name_index;
        u2 descriptor_index;
        u2 attribute_count;
        attribute_info attributes[attribute_count]
    }
  • access_flag的意义和之前field一样, 只不过取值不同, method的access flag可以取的值如下:

    手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第3张图片
    图1-3 METHOD-ACCESS-FLAG

  • name_index的意义和field的也一样, 表示了方法的名称

  • descriptor_index的意义和field也一样, 只不过其表示方法不同, 让我们来看一下它是如何表示的:

    method的descriptor由两部分组成, 一部分是参数的descriptor, 一部分是返回值的descriptor, 所以method的descriptor的形式如下:

    ( ParameterDescriptor* ) ReturnDescriptor
    

    而参数的descriptor就是field的descriptor. 返回值的descriptor也是field的descriptor但是多了一个类型就是void类型, 其的descriptor如下

        VoidDescriptor:V
    

    所以举个例子, 如果一个方法的签名是

    Object m(int i, double d, Thread t) {..}
    

    那么它的descriptor就是

    (IDLjava/lang/Thread;)Ljava/lang/Object;
    
  • attribute_count的意义和field一样表示属性的个数

  • attributes[attribute_count]和field也一样表示具体的属性, 属性的个数由attribute_count决定

3. 属性

3.1 属性结构

属性这个数据结构可以出现在class文件, 字段表, 方法表中. 有些属性是特有的, 有些属性是三个共有的.

属性的描述如下:

手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第4张图片
ATTRIBUTE1
手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第5张图片
ATTRIBUTE2
手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第6张图片
ATTRIBUTE

图1-4

这里我不会详细解释每一个属性, 我只解释一个对于实现mini jvm最重要的属性, Code Attribute, 为什么说它重要, 因为我们的函数的代码就是在Code Attribute中(实际上存储的是指令). 其他属性的一些解释可以参考oracle的jvm规范中的描述

3.2 Code Attribute

首先来看一下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 attributes_count;
    attribute_info attributes[attributes_count];
}

可以看到Code Attribute属性是非常复杂的, 下面解释一些每个成员的意义.

  • attribute_name_index指向的常量池中常量项的索引, 而且这个常量项的类型必须是UTF8 Info, 值必须是"Code"
  • attribute_length表示这个属性的长度, 但是不包括开始的6个字节
  • max_stack表示Code属性所在的方法在运行时形成的函数栈帧中的操作数栈的最大深度(真是不得不佩服java编译器, 连一个函数运行时需要的操作数栈的深度都可以计算的出来)
  • max_locals表示最大局部变量表的长度
  • code_length表示Code属性所在的方法的长度(这个长度是方法代码编译成字节后字节的长度)
  • code[length]表示的就是具体的代码, 所以说java函数的代码长度是有限制的, 编译出来的字节指令的长度只能是4个字所能代表的最大值. 所以一个函数的代码不能太长, 否者是不能编译的.
  • exception_table_length表示方法会抛出的异常数量
  • exception_table[exception_table_length]表示具体的异常
  • attributes_count表示Code属性中子属性的长度, 之所以说属性复杂就是因为属性中还可以嵌套属性
  • attributes[attributes_count]代表具体的属性

现在来直观的看一下Code Attribute的组成

手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性_第7张图片
图1-5 CODE-ATTRIBUTE3

3.3 Code Attribute的两个子属性

Code Attribute中的两个子属性对于这次的mini jvm的实现可能不是很重要, 但是它们对于调试程序是非常重要的, 不知道大家有没有想过为什么我们用IDE运行程序出错时, IDE可以准确的定位到是哪一行代码出错了? 为什么我们在断点调试的时候可以看到每一个变量的值? 很关键的原因就在于Code属性的这两个子属性.

3.3.1 LineNumberTable

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

我们着重要看的是line_number_table这个成语, 可以看到这个成员表示的就是字节码指令和源码的对应关系, 其中start_pc是Code Attribute中的code[]数组的索引值, line_number是源文件的行号

3.3.1 LocalVariableTable

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

其中最关键的成员大家也可以想到, 肯定是local_variable_table[local_variable_table_length]

  • start_pc和length表示局部变量的索引范围([start_pc, start_pc + length))
  • name_index表示变量名在常量池中的索引
  • descriptor_index表示变量描述符在常量池中的索引
  • index表示此局部变量在局部变量表中的索引

4. 总结

至于对字段, 方法, 属性的解析的代码实现这里就不描述了, 大家直接看代码实现吧.
到这篇为止, class文件的结构和解析已经全部介绍完了, 接下来的就是运行程序了, 也就是实现一个执行引擎了, 所以接下来的才是整个jvm的重点.

5. 代码地址

6. 本系列其他文章

手把手教你撸一个Mini JVM系列(1)之解析Class File -- 初探
手把手教你撸一个Mini JVM系列(2)之解析Class File -- 常量池
手把手教你撸一个Mini JVM系列(4)之执行引擎
手把手教你撸一个Mini JVM系列(5)之源码分析 -- 常量池、访问标志、类索引
手把手教你撸一个Mini JVM系列(6)之控制流 -- 条件判断和循环

你可能感兴趣的:(手把手教你撸一个Mini JVM系列(3)之解析Class File -- 字段、方法、属性)