[TOC]
Class文件介绍
Class文件是Java源代码文件经Java编译器编译后得到的Java字节码文件。对比Linux、Windows上的可执行文件而言, Class文件可以看作是Java虚拟机的可执行文件。
Class文件格式
在Java虚拟机的规范中,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];
}
各个字段的释义如下:
magic: Class文件的前4个字节是magic值,取值固定是OxCAFEBABE
minor_version: 占用两个字节,表示该Class文件的小版本号
major_version: 占用两个字节,表示该Class文件的主版本号。从JDK1.k(k≥2)开始对应的Class文件格式版本号范围是45.0 ~ (44.0+k.0)
constant_pool_count:常量池的数量
constant_pool: 常量池数组,每个元素都是一个cp_info(constant pool info)的结构,其中包括了常量的类型和常量的内容。需要注意的是常量池数组的索引从1开始,如果引用到索引为0的常量池元素则表示不引用任何常量池内容
access_flags: 表明该类的访问权限,比如public, private等
this_class: 存储了指向常量池数组元素的索引。通过常量池元素对应的内容可以得到本类的全限定类名
super_class: 存储了指向常量池数组元素的索引。通过常量池元素对应的内容可以得到父类的全限定类名
interfaces_count: 存储了该类实现了多少个接口
interfaces: 保存了实现的接口的接口名在常量池中的索引
fields_count: 保存了该类包含的成员变量的数量
fields: field_info数组,每个field_info包含了成员变量的信息
methods_count: 保存类该类中的成员方法数量
methods: method_info数组,每个method_info保存了对应的方法的信息
attributes_count: 存储了该类包含的属性信息
attributes: attributes_info数组,每个attributes_info保存了对应的属性信息。比如资源文件名等。
常量池介绍
常量项的类型
常量池中每个常量都是一个cp_info的结构;cp_info的结构如下:
cp_info{
u1 tag;
u1 info[];
}
其中,第一个字节tag表示常量项的类型,后续的info数组中是具体的常量项内容。
常量项中可能的类型有以下几种:
先从常量池的第一项来分析内容:
这里的tag值为10,参照常量项和tag值对照表能知道第一个cp_info是一个CONSTANT_Methodref
类型。接下来的的class_index
的值7指向了常量池的第7项。
再看第七项的内容,再第七项中tag值为7,表示这是一个CONSTANT_Class类型的结构。后续的name_index
又指向了常量池的第33项。
可以看到这里的tag值是1,表示这里是一个CONSTANT_Utf8
类型结构。CONSTATN_Utf8
结构中存储的是字符串常量值。下一项是length,表示这个字符串常量的长度。这里的值是27。再接着就是字符串的具体值数组,可以看到这里的字符串值是当前类的父类的全限定名。
然后再回到常量池的第一项中的name_and_type_index
部分,这里又指向了常量池的第25项。
在第25项中可以看到它的tag值是12,类型是CONSTANT_NameAndType
。接下来的name_index
指向了常量池的第11项。
这里又指向了一个CONSTANT_Utf8
类型结构,其中存储的字符串值是<init>
。
在看25项中的descriptor_index
又指向了常量池的第12项,可以看到第12项中存储的字符串是()V
所以把字符串常量池的第一项内容全部拼接起来就是一串com/suifeng/lint/HelloSuper "<init>" ()V
。可以从这一串字符串中看出该方法所在类是com/suifeng/lint/HelloSuper
。方法名是<init>
也就是HelloSuper
类的构造函数,该函数的返回值是Void
。
至于为什么采用间接引用索引的方式是为了减小Class文件的空间占用。
常量项的结构
cp_info
cp_info{
u1 tag;
u1 info[];
}
这是一个大体结构,所有的常量项,不论是CONSTANT_Class
还是CONSTANT_Fieldref
还是其他的任意类型,都遵守这个结构。
结构的第一个字节表示了当前结构的类型,后续为该结构的具体内容。
Class_info
CONSTANT_Class_info {
u1 tag; //7,表示CONSTANT_Class_info类型
u2 name_index; // 保存了索引,指向了保存当前类的全限定名字符串的常量项
}
field_info
CONSTANT_Fieldref_info {
u1 tag; // 9,表示CONSTANT_Fieldref_info类型
u2 class_index; // 保存了索引,指向了保存当前类的全限定名字符串的常量项
u2 name_and_type_index; // 保存了索引,指向了field的名称和类型
}
method_info
CONSTANT_Methodref_info {
u1 tag; // 10,表示CONSTANT_Methodref_info类型
u2 class_index; // 保存了索引,指向了保存当前类的全限定名字符串的常量项
u2 name_and_type_index;// 保存了索引,指向了method的名称、传参和返回值等信息
}
常量信息描述规则
基础数据类型
基础数据类型按照如下的对应关系:
基础数据类型 描述符 byte B char C double D float F int I long L short S boolean Z 这里要注意的是,所有的描述符都是大写的。
引用数据类型
引用数据类型的格式为
LClassName;
,这里的ClassName必须是类的全限定名称,比如Ljava/lang/Object;
,全限定名称中的.
要换成/
,并且最后要加上分号。数组类型
数组也是一种引用类型,数组用以
[
开头加上引用数据类型的方式来表示。例如表示一个Object数组,可以用[Ljava/lang/Object;
的方式来表示。如果是二位数组,可以以[[
开头,后面加上对应的应用数据类型或者基础数据类型。比如一个二维int数组可以用[[I
来表示。对于多维数组,有几维就放几个[
来表示维数。
field_info和method_info
field_info的结构如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count]
}
method_info的结构如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count]
}
可以看到两者的结构非常相似,这里做同意解释:
access_flags: 访问标识,但要注意field的访问标识和method的访问标识有所不同,后面会讲到
name_index: 存储了指向成员变量名或成员函数名的
CONSTANT_Utf8_info
常量项descriptot_index: 存储了指向成员变量描述符(变量类型)或成员函数描述符(函数的传参和返回值)
attributes_count: 当前变量或函数携带的属性个数
attributes: 当前变量或函数携带的属性值,以
attribute_info
数组的形式存在
access_info介绍
在Java中,类,类的成员都有访问控制,比如public或者private等在Class文件中都转换成access_flags。每种访问信息都由一个十六进制的标志值表示,如果同时具有多种访问信息,则得到的标志值为这几种访问信息的标志值的逻辑或。
class的access_flags取值如下:
field的access_flags取值如下:
method的access_flags取值如下:
属性介绍
属性是Class文件的重要组成部分。和常量池类似,属性也分很多类型。在Java虚拟机规范中,属性可用attribute_info数据结构伪代码表示
attribute_info {
u2 attribute_name_index; // 属性名称,指向常量池中Utf8常量项的索引
u4 attribute_length; // 该属性具体内容的长度,即下面info数组的长度
u1 info[attribute_length]; // 属性具体内容
}
和常量池类型不一样的是,属性是由其名称来区别的,即attribute_info中的attribute_name_index所指向的Utf8字符串。下面列出了一些重要属性的名称和它们的作用。
从上述介绍中可知
属性的类型由其名字来描述。比如”Code””SourceFile”等
不同类型的属性可能出现在ClassFile中不同的成员里,或者某个属性只能出现在固定的成员中。
属性也可以包含子属性,比如”Code”属性能包含”LocalVariableTable”属性。
Code属性
Code属性中包含了一个函数的内容,是很重要的一个属性。
Code属性的结构如下:
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指向内容为”Code”的Utf8_info常量项。attribute_length表示接下来内容的长度。
max_stack:JVM执行一个指令的时候,该指令的操作数存储在一个名叫“操作数栈(operand stack)”的地方,每一个操作数占用一个或两个(long、double类型的操作数)栈项。stack就是一块只能进行先入后出的内存。max_stack用于说明这个函数在执行过程中,需要最深多少栈空间(也就是多少栈项)。
max_locals: 表示该函数包括最多几个局部变量。注意,max_stack和max_locals都和JVM如何执行一个函数有关。根据JVM官方规范,每一个函数执行的时候都会分配一个操作数栈和局部变量数组。所以Code_attribute需要包含这些内容,这样JVM在执行函数前就可以分配相应的空间。
code_length和code:函数对应的指令内容也就是这个函数的源码经过编译器转换后得到的Java指令码存储在code数组中,其长度由code_length表明。
exception_table_length和exception_table:一个函数可以包含多个try/catch语句,一个try/catch语句对应exception_table数组中的一项
start_pc描述try/cath语句从哪条指令开始。注意,这个table中的各个pc变量的取值必须位于代表整个函数内容的Java字节码code[code_length]数组中。
end_pc表示这个try语句到哪条指令结束。注意,只包括try语句,不包括catch。
handler_pc表示catch语句的内容从哪条指令开始
catch_type表示catch中截获的Exception或Error的名字,指向Utf8_info常量项。如果catch_type取值为0,则表示它是final{}语句块。
另外,图2-7中表明Code_atrribute还能包含其他属性,Code_attribute里常见的属性有:LineNumberTable用于调试,比如指明哪条指令。对应于源码哪一行。
LocalVariableTable用于调试,调试时可以用于计算本地变量的值。
LocalVariableTypeTable,功能和LocalVariableTable类似。
StackMapTable为Java 1.6以上才支持的属性。JVM加载Class文件的时候,将利用该属性的内容对函数进行类型校验(Type Checking)。