JAVA虚拟机系列(六)

来源:互联网 发布:女性情趣用品淘宝 编辑:程序博客网 时间:2024/05/22 13:23

Java能够做到一处编写到处运行,它的主要原因是使用Java虚拟机实现了底层的隔离,同时Java虚拟机执行统一的数据格式,即字节码文件,对于字节码文件有统一以及严格的规定,使得字节码文件可以运行到任何虚拟机上,从而实现了任意平台的可移植性,对于不同的操作系统,只需下载相对应的版本的虚拟机即可,内部的字节码的规定没有任何变化。

下来就让我们进行简单的介绍字节码文件的格式,对于想详细了解字节码文件格式的好学者,可以阅读《深入了解Java虚拟机》以及上机进行实际操作。

时至今日,商业机构和开源机构已经在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Clojure,Groovy,Jruby等等,

1.Class类文件的结构

本章节介绍的Class类文件结构的讲解中,以《Java虚拟机规范》(1999年发布,对应JDK1.4时代的Java虚拟机)中的定义为主线。

任何一个Class文件都对应唯一一个类或者接口的定义信息,但反过来说,类和接口信息并不一定都得定义在文件里。

Class文件是一个以8位字节为基础的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有任何间隔,当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。Class文件不像XML等描述语言,由于它没有任何分隔符号,所以他的数据项,无论是顺序还是数量,这些细节,都是被严格限定的,哪些字节代表什么含义,长度是多少,先后顺序都不允许改变。Class文件格式采用类似于C语言结构体的伪结构来存储数据,伪结构只有两种数据类型:无符号数和表,无符号数属于基本的数据类型,以u1,u2,u4,u8来代表1个字节,2个字节,4个字节,8个字节,无符号数用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串值。表是由多个无符号数或者其他表作为数据项构成的复合类型的数据。

(1)魔数与Class文件的版本

每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。在很多文件的存储标准中都使用魔数来进行身份识别,例如gifjpeg等在头文件中都存有魔数,而不是使用扩展名来进行识别,主要是基于安全方面的考虑,因为扩展名可以任意改动。

魔数下来是紧接着的是Class文件的版本号,第5和第6个字节的次版本号,第78是主版本号,Java的版本号是从45开始,JDK1.1之后的每个JDK大版本发布主版本号向上加1,版本直接都是向下兼容,不能向上兼容,即高版本可以运行低版本,低版本不可以运行高版本。如果想查看字节码文件的16进制,可以使用WinHex来进行转换。

(2)常量池

紧接着主版本号的是常量池。它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时他还是Class文件中第一个出现的表类型的数据项目。

常量池的常量的数量是不确定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。与Java中语言习惯不一样的是,当个容量计数是从1开始而不是从0,设计者将0空出来,是为了在需要表达不引用任何一个常量池项目的含义。

(3)访问标志

在常量池结束之后,紧接着的是两个字节代表的访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括Class是类还是接口,是否定义为Public类型,是否定义为abstract类型,如果是类,是否被声明为final等。

(4)类索引,父类索引与接口索引集合

类索引和父类索引是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父索引引用用于确定这个类的父类的全限定名。由于Java只接受单继承,所以父类索引只有一个,即除了Java.lang.Object之外,其他的类都有父类。

类索引,父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,对于接口类索引集合,入口的第一项-u2类型的数据为接口计数器,表示索引表的容量,如果没有实现任何接口,则该计数器的值为0

(5)字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。它包括的信息有:字段的作用域,是实例变量还是类变量,可变性,并发可见性,可否被序列化,字段数据类型,字段名。

(6)方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方发表的结构如同字段表一样,依次包括了访问标志,名称索引,描述符索引几项。方法中的Java代码,经过编译器译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面。

(7)属性表集合

Class文件,字段表,方发表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

属性表集合的限制稍微宽松一些,不在要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,Java虚拟机运行时会忽略他不认识的属性。

Code属性

Java程序的方法中的代码经过Java编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表中的属性集合中,但并非所有额方法表都必须存在这个属性,例如接口或者抽象类中的方法就不存在Code属性。

Exceptions属性

Exceptions属性在方法表中与Code属性平级的一项属性,Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面的异常。

LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系。并不是运行时必须的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:lines选项来取消或要求生成这项信息。

LocalVaribleTable属性

用于描述栈帧中局部变量中的变量与Java源码中定义的变量之间的关系,他也不是运行时必须的属性,默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:vars选项来取消或者要求生成这项信息。

ConstantValue属性

用于通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。等等还有innerClasses属性,Deprecated以及Synthetic属性,stackMap Table属性,Signature属性,BootstrapMethods属性。

(8)字节码指令简介

Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称作操作码)以后跟随其后的零至多个代表此操作所需参数而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令不包含操作数,只有一个操作码。由于限制了Java虚拟机操作码的长度为一个字节,这意味着指令集的操作码总数不可能超过256条,又由于Class文件格式放弃了编译后代码的操作数长度对齐,意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时重建出具体数据的结构。

字节码与数据类型:Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型的信息。例如,float指令加载的则是float类型的数据。

加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括讲一个局部变量加载到操作栈,将一个数值从操作栈存储到局部变量表,将一个常量加载到操作数等等所对应的具体指令。

运算指令:运算指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。它包括加法,减法,乘法,除法,取反,求余指令等等。

类型转换指令:类型转换指令可以将两种不同的数值类型进行相互转换,这些操作一般用于实现用于代码中的显示类型转换操作。这里涉及到宽化类型转换即小范围类型想大范围类型的安全转换;窄化转换,窄化处理过程会导致数据的精度丢失,

对象创建与访问指令:创建对象的指令new,创建数组的指令newarray,anewarray,multianewarray,取数组长度的指令arraylength

操作数栈管理指令:即操作数栈的栈顶一个或两个元素出栈,复制栈顶元素,栈顶连个数值互换。

还有控制转移指令,方法调用和返回指令,异常处理指令,同步指令等等功能强大的指令。

本章节简单的介绍了类文件的格式以及一些指令,但是对于每个指令以及每个标题的介绍只是其中的一部分而已,其中的更具体的内容还请读者尽量看书,可以更进一步的理解。如有错误,敬请指出。