虚拟机和字节码
实现语言无关性的基础是虚拟机和字节码存储格式。JVM可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”。Java虚拟机不和包括Java在内的任何语言绑定,只与“Class文件”这种特定的二进制文件所关联。Class文件中包含了Java虚拟机指令集合符号表以及若干其它辅助信息。Java虚拟机作为一个通用的、机器无关的执行平台,任何其他语言都可以将其作为语言的产品交付媒介。
Class类文件结构
一个Class文件都对应唯一一个类或接口的定义信息,但是反过来,类或接口并不一定都定义在文件里(类或接口也可以通过类加载器直接生成)
Class文件是一组以8位字节为基础的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数,这种伪结构有两种数据类型:无符号数和表。Class文件结构不像XML等描述语言,由于它没有任何分割符号,所以无论是数量甚至于数据存储的字节序这样的细节都被严格限定,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
无符号数是基本的数据类型,以u1,u2,u4,u8分别代表对应字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表示由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯以“_info”结尾。表用于描述有层次关系的复合结构的数据。整个Class文件本质就是一张表。
| 类型 | 名称 | 数量 |
|---|---|---|
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count-1 |
| u2 | access_flags | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interfaces_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | method_count |
| u2 | attribute_count | 1 |
| attribute_info | attributes | attributes_count |
魔数与版本号
- magic
class文件的开头的四个字节,成为魔数,用来确定这个文件是否是一个能被JVM识别的class文件。它是一个固定值:0XCAFEBABE,如果开头四个字节不是这个值,说明不是class文件,不能被JVM识别。
- minor_version和Major——version
第5-8字节存放的是Class文件的版本号。5-6存放次版本号,7-8存放主版本号。高版本的JVM能识别低版本的javac编译器编译的class文件,但是低版本的JVM不能识别高版本的class文件。如果使用低版本的JVM执行高版本的class文件,JVM会抛出java.lang.UnsupportedClassVersionError。
常量池
版本号后边是常量池入口,是Class文件中的资源仓库。是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,是第一个出现的表类型项目。Class
由于常量池中常量数目不固定,在其入口放置了一个u2类型的数据表示常量池容量计数值。计数值从1开始,如十进制为22,有1-21共21项常量,下标为0的位置空出,如果class文件中的其他地方引用了索引为0的常量池项,就说明它不引用任何常量池项。除了常量池之外,其余技术都是从0开始的。
常量池存放两大常量:字面量和符号引用。
字面量类似于java语言层面的常量,如字符串、声明为final的常量值。符号引用则属于编译原理方面的概念,包括以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符 每个类只有一个常量池,其中的数据也是一项一项没有间隙的一次排放,各个数据项通过索引访问。常量池中每一项常量都是一个表。JDK1.7支持14种项目类型。这14种表开始的第一位是一个u1类型的标志位,代表当前常量属于哪种常量类型。
Class文件中的方法、字段等都需要引用CONSTANT_Utf8_info型常量描述名称,其长度为u2的最大值65535,所以java程序中定义了超过64KB英文字符的变量或方法名,会无法编译。Class文件的字节码内容可以通过javap -verbose YourClass来查看。
访问标志
常量池结束后,紧接着两个字符代表access_flags,用于标识一些类或接口层次的访问信息:Class是类还是接口;是否定义为public或abstract;如果是类,是否声明为final。
类索引、父类索引与接口索引集合
Class文件通过类索引(this_class)和父类索引(super_class)以及接口索引集合(interfaces)这三项数据来确定这个类的继承关系。
类索引用来确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。java.lang.Object没有父类,其父类索引为0。
接口索引集合用来描述这个类实现了哪些接口,被实现的接口按照implements后的接口顺序从左到右排列在接口索引集合中。如果本就是接口,看extends后的接口。其入口是一个u2类型的接口计数器。
字段表集合
field_info字段表用于描述接口或者类中声明的变量。包括了类级变量和实例变量,但是不包括在方法内部声明的局部变量。
字段的修饰符包括:作用域(public ,private,protected修饰符),是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)。这些修饰符都是布尔值,使用标志位来表示。在field_info表中,使用access_flags来表示。
字段的数据类型和字段的名称是无法固定的,需要引用常量池中的常量来描述。使用name_index和descriptor_index来分别代表字段的简单名称和字段和方法的描述符。后序字段为属性表集合,在下方介绍。
- 全限定名:如com/example/clazz/Class就是一个类的全限定名。全限定名一般在最后加入一个;表示全限定名的结束。
- 简单名称:没有类型和参数修饰的方法或字段名。如int c的简单名称为c;public int getValue()的简单名称为getValue。
- 描述符:描述字段的数据类型、方法的参数列表(数量、类型以及顺序)和返回值。基本数据类型和void都用一个大写字符表示,对象类型使用字符L加对象的全限定名来表示。数组类型,每一个维度使用一个前置[描述。方法的描述按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()内。如 String myFunc(int[] a)的描述符([I)Ljava/lang/String;
方法表集合
Class文件中方法表中对方法的描述和字段的描述几乎采用了完全一致的方式。包括access_flag,name_index,descriptor_index,attributes_count,attributes几个部分。
因为volatile修饰符和transient修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized、native、abstract等关键字修饰方法,所以也就多了这些关键字对应的标志。
属性表集合
在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
- Code属性:Java程序方法体重的代码经过javac编译器处理后,变为字节码指令存储在Code属性内。Code属性是Class文件中最重要的一个属性,Code属性用于描述代码,其他数据项目则用于描述元数据。(元数据,包括类、字段、方法定义以及其他信息)。
- Exceptions属性:用于列举出方法中可能抛出的受查异常,也就是thorws关键字后面列举的异常。
- LineNumberTalbe属性:用于描述Java源码行号与字节码行号的对应关系。(可选)
- LocalVariableTable属性:描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。(可选)
- SourceFile属性:记录生成该Class文件的源码文件名称。(可选)
- ConstantValue属性:通知虚拟机自动为静态变量赋值。只有被static修饰的变量才能使用这项属性。
- InnerClasses属性:记录内部类与宿主类之间的关联。
- Deprecated和Syntheic属性:布尔类型。前者表示不再推荐使用,可以用@deprecated注解设置。后者表示该字段或方法不是有Java源码直接产生的,而是编译器自行添加的。
参考
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- mp.weixin.qq.com/s?__biz=MzU…
- blog.csdn.net/u010425776/…