手把手教你撸一个Mini JVM系列(3)之解析Class Fi
引子:
在常量池, 访问修饰符, 类和接口后面紧跟的内容是字段和方法, 这两个结构是最复杂的, 因为其里面包含有属性这个成员, 而属性又是可以嵌套的.
1. 字段
和之前的常量池一样, 因为每个class中字段的数量是不确定的, 所以字段部分的开头两个字节用于表示当前class文件中的字段的个数, 紧跟着的才是具体的字段.
先来看一下字段的结构
Field_Info {
u2 access_flag;
u2 name_index;
u2 descriptor_index;
u2 attribute_count;
attribute_info attributes[attribute_count];
}
-
access_flag表示该字段的访问修饰符, 字段的访问修饰符和类的表示方式相似, 但是具体的内容不一样
字段的访问标识
图1-1 FIELD-ACCESS-FLAG -
name_index指向常量池中的name_index索引的常量项
-
descriptor_index指向常量池中的descriptor_index索引的常量项
-
attribute_count表示该字段的属性个数
-
attributes[attribute_count]表示该字段的具体的属性
注意: 这里字段的descriptor代表的字段的类型, 但是类型不是写代码的时候int, String这样整个单词的, 它是一些字符的简写, 如下:
图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可以取的值如下:
图1-3 METHOD-ACCESS-FLAG
<div style="margin-left:200px"></div>
-
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文件, 字段表, 方法表中. 有些属性是特有的, 有些属性是三个共有的.
属性的描述如下:
ATTRIBUTE1 ATTRIBUTE2 ATTRIBUTE<div style="margin-left:200px">图1-4</div>
这里我不会详细解释每一个属性, 我只解释一个对于实现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的组成
图1-5 CODE-ATTRIBUTE33.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)之控制流 -- 条件判断和循环