虚拟机的类加载和执行机制是虚拟机的最主要功能,在这里简单的对所知的内容进行一次温习,并记录以方便日后重温。 本篇主要引用《深入理解Java虚拟机——JVM高级特性与最佳实践》一书。 1、类文件结构 java虚拟机要对类文件进行加载和执行,那么必须要能够理解类文件结构,而对于虚拟机而言,平台无关性和语言无关性是其最重要的两大特征,那么就势必要对类文件结构进行规范化和结构化,这样才能保证无论是什么语言编译成的字节码文件,java虚拟机都能够正常加载和执行。因此,对于字节码文件(即.class文件)的简单理解是进一步理解虚拟机运行机制的基本步骤。 Class类文件,亦称字节码文件,是由虚拟机规范规定了其结构形式的文件。Class文件是一组以8位为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有任何分隔符,以保证整个Class文件中存储的内容全部是程序运行的必要数据,没有空隙。当遇到需要占用8位字节以上空间的数据时,则会按照高位在前的方式分割成若干上8位字节进行存储。 根据虚拟机规范的规定,Class文件格式中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。表是多个无符号数或其它表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合。 说完了虚拟机规范对Class文件的基本约定后,我们来关注一下Class文件都有些啥。 既然虚拟机是语言无关,那我们可以以java语言作为范本进行学习。回顾一下,我们在定义一个类的时候,都需要或者说可以定义些什么内容。首先,类的修饰符,是abstract,或public、protected、private,然后是类名,再接着,是否有继承或是实现父类或接口,这些是类的基本约束。再接着来看类的内容,我们可以定义类成员变量(static)和实例成员变量,接着是定义类的行为——类方法,类方法又有方法名,返回值,参数值,还有异常列表等。 由上面这些定义的内容,我们可以猜到,当这个定义的类被编译成Class文件时,Class文件中应该要包含些什么内容了。我们再从虚拟机的角度来完整地了解Class文件的结构。 首先,最简单的一个问题,虚拟机必须判定输入的文件是不是一个Class文件,java虚拟机通过识别输入文件的首4个字节的魔数(0xCAFEBABE)来确定其是否Class文件。接着虚拟机由于一直在不断地改进和更新,所以不断有新的版本出现,新的版本能兼容旧的版本,但旧的版本可能就完全无法读取新的虚拟机编译而成的Class文件了,因此,虚拟机就必须对Class文件进行版本的识别和检查,也就是说,Class文件必须要有版本号的数据(Class文件的第5到第8个字节)。 紧接着是Class文件的常量池入口,常量池是Class文件结构中与其它项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时也是在Class文件中第一个出现的表类型数据项目。由于常量池中常量的数量不固定,因此在常量池的入口之前有一个u2类型的容量计数值。常量池之中主要存放两大常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量即是java语言层面中的常量,如文本字符串(如"adb"等字面量),被声明成final的常量值。符号引用则属于编译原理方面的概念,包括三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。这些符号引用在虚拟机中如果不经过转换则无法与实际内存相连接,即无法被虚拟机直接使用,在虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址中。每项常量都是一个表,而由于各个常量的类型不一,大小也不相同,所以同样需要一个u1类型的数据来标记常量的类型,以确定其后的常量表的格式。 在常量池之后,紧接着的2个字节代表访问标志,即在前面说到的,这个Class是类还是接口,是用哪个修饰符来修饰,abstract,public等,还有,如果是类的话,是否被声明为final,等等。 访问标志之后,则是类索引、父索引与接口索引的集合。类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用来确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按实现或继承的顺序从左到右的顺序排列在接口的索引集合中。类索引、父类索引和接口索引都按顺序排列在访问标志之后。 接下来就是字段表了,此处字段表存的就是前文说的类成员变量或实例成员变量,但不包括方法内部声明的变量。如果类存在父类,则除非子类覆盖了父类的字段定义,否则在子类中不会列出从超类或父接口中继承而来的字段,但有可能列出原来java代码中不存在的字段,譬如在内部类为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,java中是不允许出现相同的字段名的,但对于字节码来说,如果两个字段的描述符不一致,则字段重名是合法的。 字段表之后就是方法表集全了。方法表集合与字段表集合的结构形式几乎完全一致。此处,方法中的代码的存放位置则是方法表的属性表中的一项名为"Code"的属性里面。与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),则方法表集合中就不会出现来自父类的方法信息。 最后来对上面说到的属性表作个解释。属性表是Class文件格式中最具扩展性的一种数据项目,在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息(如方法表中专有的代码信息),具体的属性表的各个属性项目若有兴趣可以翻看《深入理解java虚拟机》这本书,也可以直接翻看虚拟机规范。
|