最近在写一个私人项目,名字叫作ClassAnalyzer
,ClassAnalyzer
的目的是能让咱们对Java Class
文件的设计与结构可以有一个深刻的理解。主体框架与基本功能已经完成,还有一些细节功能往后再增长。实际上JDK
已经提供了命令行工具javap
来反编译Class
文件,但本篇文章将阐明我实现解析器的思路。java
做为类或者接口信息的载体,每一个Class
文件都完整的定义了一个类。为了使Java
程序能够“编写一次,到处运行”,Java虚拟机规范对Class
文件进行了严格的规定。构成Class
文件的基本数据单位是字节,这些字节之间不存在任何分隔符,这使得整个Class
文件中存储的内容几乎所有是程序运行的必要数据,单个字节没法表示的数据由多个连续的字节来表示。git
根据Java
虚拟机规范,Class
文件采用一种相似于C
语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。Java
虚拟机规范定义了u1
、u2
、u4
和u8
来分别表示1
个字节、2
个字节、4
个字节和8
个字节的无符号数,无符号数能够用来描述数字、索引引用、数量值或者是字符串。表是由多个无符号数或者其它表做为数据项构成的复合数据类型,表用于描述有层次关系的复合结构的数据,所以整个Class
文件本质上就是一张表。在ClassAnalyzer
中u1
、u2
、u4
和u8
分别对应于byte
、short
、int
和long
,Class
文件被描述为以下Java
类。github
public class ClassFile {
public U4 magic; // magic
public U2 minorVersion; // minor_version
public U2 majorVersion; // major_version
public U2 constantPoolCount; // constant_pool_count
public ConstantPoolInfo[] cpInfo; // cp_info
public U2 accessFlags; // access_flags
public U2 thisClass; // this_class
public U2 superClass; // super_class
public U2 interfacesCount; // interfaces_count
public U2[] interfaces; // interfaces
public U2 fieldsCount; // fields_count
public FieldInfo[] fields; // fields
public U2 methodsCount; // methods_count
public MethodInfo[] methods; // methods
public U2 attributesCount; // attributes_count
public BasicAttributeInfo[] attributes; // attributes
}复制代码
组成Class
文件的各个数据项中,例如魔数、Class
文件的版本、访问标志、类索引和父类索引等数据项,它们在每一个Class
文件中都占用固定数量的字节,在解析时只须要读取相应数量的字节。除此以外,须要灵活处理的主要包括4
部分:常量池、字段表集合、方法表集合和属性表集合。字段和方法均可以具有本身的属性,Class
自己也有相应的属性,所以,在解析字段表集合和方法表集合的同时也包含了属性表的解析。框架
常量池占据了Class
文件很大一部分的数据,用于存储全部的常量信息,包括数字和字符串常量、类名、接口名、字段名和方法名等。Java
虚拟机规范定义了多种常量类型,每一种常量类型都有本身的结构。常量池自己是一个表,在解析时有几点须要注意。工具
u1
类型的tag
来标识。constantPoolCount
)比实际大1
,例如,若是constantPoolCount
等于47
,那么常量池中有46
项常量。1
开始,例如,若是constantPoolCount
等于47
,那么常量池的索引范围为1~46
。设计者将第0
项空出来的目的是用于表达“不引用任何一个常量池项目”。CONSTANT_Utf8_info
型常量的结构中包含u1
类型的tag
、u2
类型的length
和由length
个u1
类型组成的bytes
,这length
字节的连续数据是一个使用MUTF-8
(Modified UTF-8)
编码的字符串。MUTF-8
与UTF-8
并不兼容,主要区别有两点:一是null
字符会被编码成2
字节(0xC0
和0x80
);二是补充字符是按照UTF-16
拆分为代理对分别编码的,相关细节能够看这里(变种UTF-8)。属性表用于描述某些场景专有的信息,Class
文件、字段表和方法表都有相应的属性表集合。Java
虚拟机规范定义了多种属性,ClassAnalyzer
目前实现了对经常使用属性的解析。和常量类型的数据项不一样,属性并无一个tag
来标识属性的类型,可是每一个属性都包含有一个u2
类型的attribute_name_index
,attribute_name_index
指向常量池中的一个CONSTANT_Utf8_info
类型的常量,该常量包含着属性的名称。在解析属性时,ClassAnalyzer
正是经过attribute_name_index
指向的常量对应的属性名称来得知属性的类型。this
字段表用于描述类或者接口中声明的变量,字段包括类级变量以及实例级变量。字段表的结构包含一个u2
类型的access_flags
、一个u2
类型的name_index
、一个u2
类型的descriptor_index
、一个u2
类型的attributes_count
和attributes_count
个attribute_info
类型的attributes
。咱们已经介绍了属性表的解析,attributes
的解析方式与属性表的解析方式一致。编码
Class
的文件方法表采用了和字段表相同的存储格式,只是access_flags
对应的含义有所不一样。方法表包含着一个重要的属性:Code
属性。Code
属性存储了Java
代码编译成的字节码指令,在ClassAnalyzer
中,Code
对应的Java
类以下所示(仅列出了类属性)。spa
public class Code extends BasicAttributeInfo {
private short maxStack;
private short maxLocals;
private long codeLength;
private byte[] code;
private short exceptionTableLength;
private ExceptionInfo[] exceptionTable;
private short attributesCount;
private BasicAttributeInfo[] attributes;
...
private class ExceptionInfo {
public short startPc;
public short endPc;
public short handlerPc;
public short catchType;
...
}
}复制代码
在Code
属性中,codeLength
和code
分别用于存储字节码长度和字节码指令,每条指令即一个字节(u1
类型)。在虚拟机执行时,经过读取code
中的一个个字节码,并将字节码翻译成相应的指令。另外,虽然codeLength
是一个u4
类型的值,可是实际上一个方法不容许超过65535
条字节码指令。命令行
ClassAnalyzer
的源码已放在了GitHub上。在ClassAnalyzer
的README中,我以一个类的Class
文件为例,对该Class
文件的每一个字节进行了分析,但愿对你们的理解有所帮助。翻译