代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。
使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是何种语言。
image.png
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大
。因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。
Class文件是一组以8位字节为基础单位的二进制流
,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数
和表
:
-
无符号数
属于基本的数据类型,以u1
、u2
、u4
、u8
来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
。 -
表
是由多个无符号数或者其他表作为数据项构成的复合数据类型
,所有表都习惯性地以"_info"结尾
。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表
Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式
,这时称这一系列连续的某一类型的数据为某一类型的集合。
1、魔数
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。
Class文件的魔数为:0xCAFEBABE
2、Class文件版本号
紧接着魔数的4个字节存储的是Class文件的版本号:
第5和第6个字节是次版本号(Minor Version)
,第7和第8个字节是主版本号(Major Version)
Java的版本号是从45开始
的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0-1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
3、常量池
常量池数据区.png常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
,常量池容量为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的
,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始。
常量池中主要存放两大类常量:字面量(Literal)
和符号引用(Symbolic References)
。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
常量池中每一项常量都是一个表,在JDK 1.7之后共有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位
常量池项数据结构.png 常量池中数据类型.png以上各种类型在常量池冲的标识和存储如下所示:
-
int和float
int和float.png -
long和double
long和double.png -
UTF-8编码的字符串
UTF-8编码的字符串.png -
String类型的字符串常量
String类型的字符串常量.png
CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。如下图所示: 字符串.png
我们可以看到CONSTANT_String_info结构体位于常量池的第#8个索引位置。而存放"Java虚拟机原理" 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,该结构体位于常量池的第#8个索引位置 -
类名
VM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7 。其结构如下: 类.png -
Field字段
CONSTANT_Fieldref_info.png
CONSTANT_Name_Type_info.png
字段描述符.png -
method方法
CONSTANT_Methodref_info.png
4、访问标志、类索引、父类索引、接口索引集合
访问标志、类索引、父类索引、接口索引集合 在class文件中的位置如下图所示: 访问标志、类索引、父类索引、接口索引集合.png-
访问标志(access_flags)
访问标志(access_flags)紧接着常量池后,占有两个字节,总共16位,如下图所示: 访问标志.png -
类索引(this_class)
一般情况下一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过 类索引 这个数据项来确定。
类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。 类索引紧接着访问标志的后面,占有两个字节,在这两个字节中存储的值是一个指向常量池的一个索引,该索引指向的是CONSTANT_Class_info常量池项
类索引.png -
父类索引(super_class)
Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。 -
接口索引集合(interfaces)
一个类可以不实现任何接口,也可以实现很多个接口,为了表示当前类实现的接口信息,class文件使用了如下结构体描述某个类的接口实现信息:
接口索引集合.png
由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节
5、字段表集合
字段表(field_info)
用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
字段表集合在class文件中的位置:
字段表集合在class文件中的位置.png field_info结构体信息.png
FIeld_info的组成元素:
字段的作用域(public、private、protected修饰符)
、是实例变量还是类变量(static修饰符)
、可变性(final)
、并发可见性(volatile修饰符,是否强制从主内存读写)
、可否被序列化(transient修饰符)
、字段数据类型(基本类型、对象、数组)
、字段名称
class文件对数据类型的表示如下图所示:
如果使用final和static同时修饰一个field字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;如果该field字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法<cinit>()中赋值。
对于public static final init MAX=100; javac编译器在编译此field字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。
FIeld_info ConstantValue.png
6、方法表集合
方法表集合是指由若干个方法表(method_info)
组成的集合。对于在类中定义的方法,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:
method方法的描述-方法表集合在class文件中的位置如下所示:
方法表集合在class中的位置.png
method_info结构体的定义如下所示:
method_info结构体的定义.png
该结构体的定义跟描述field字段 的field_info结构体的结构几乎完全一致,如下图所示:
method_info.png
方法表的结构体由:访问标志(access_flags)
、名称索引(name_index)
、描述索引(descriptor_index)
、属性表(attribute_info)
集合组成。
-
访问标志(access_flags):
method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息, -
名称索引(name_index):
紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。如public void methodName(),很显然,“methodName”则表示着这个方法的名称,那么在常量池中会有一个CONSTANT_Utf8_info格式的常量池项,里面存储着“methodName”字符串,而mehodName()方法的方法表中的名称索引则指向了这个常量池项。 -
描述索引(descriptor_index):
方法描述符.png
描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为:(参数数据类型描述列表)返回值数据类型
-
属性表(attribute_info)集合:
这个属性表集合非常重要,方法实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现
7、属性表集合
未完待续
参考:
《Java虚拟机原理图解》
网友评论