详细请阅读《深入理解Java虚拟机》第六章节。
1、概述
平台无关性(Write Once ,Run anyWhere)、语言无关性(让其他语言运行在Java虚拟机之上,如Groovy、Scala、JRuby、Jython、Clojure等)的基础是虚拟机和字节码文件。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。
2、Class类文件的结构
Class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,u1,u2,u4,u8来分别代表1、2、4、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表习惯以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质是一张表。
注意:Class文件不像XML等描述语言,由于它没有任何分隔符,无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的,哪一个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
2.1、魔数与Class文件的版本
每个Class文件的头四个字节被成为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,比如图片格式。
紧接着魔数的四个字节存储的是Class文件的版本号:第五和第六字节是次版本号,第七和第八是主版本号。Java版本号从45开始,JDK1.1之后的每一个JDK大版本发布主版本号都会向上加一(JDK1.0~JDK1.1使用了45.0~45.3的版本号),高版本号的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。例如JDK1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件。
2.2、常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时也是在Class文件中第一个出现的表类型数据项目。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。
常量池中主要存放两大类常量:字面量和符号引用。
字面量:接近于Java语言层面的常量概念,比如文本字符串、声明为final的常量值等。
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
Java代码在进行javac编译的时候,并不像c或c++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
常量池中的每一项常量都是一个表。JDK1.7中共有14种结构各不相同的表结构数据,这14种表都有一个共同的特点。就是表开始第一位是一个u1类型的标志位,代表当前的这个常量是哪一种常量类型。
Java中如果定义了超过64KB(65535)英文字符的变量或者方法名,将无法编译。由于Class文件中的方法、字段等都需要引用CONSTANT_Utf8_info行常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是u2类型能表达的最大值65535.
2.3、访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。包括:这个Class是类还是接口;是否定义为public类型;是否定义为Abstract类型;如果是类的话,是否被声明为final等
2.4、类索引、父类索引与接口索引集合
类索引和父类索引都是一个u2类型的数据,而接口索引是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串,
2.5、字段表集合
字段表用于描述接口或类中声明的变量。字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。字段的信息:作用域(private 、public、protected)、可变性(final)、并发可见性(volatile,是否强制从主内存读取)、字段类型(基本类型、对象、数组)、是否可序列化(transient)、字段名称、是类变量(static)还是实例变量。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符要么没有。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
这里有一个概念需要了解:描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符的规则,基本数据类型(byte、short、int、long、float、double、char、boolean)以及代表无返回值的void类型都用一个大写的字符表述,而对象类型用字符L加对象的全限定名来表示。
如下图所示,字段表(方法表与字段表的结构一样)的结构:
2.6、属性表
在Class文件、字段表、方法表、都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
Code属性:Java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。
Exceptions属性:列举出方法中可能抛出的受检查异常,也就是方法描述时在throws关键字后面列举的异常。
LineNumberTable属性:描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable属性:用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。
SourceFile属性:用于记录生成这个Class文件的源码文件名称。
ConstantValue:通知虚拟机自动为静态变量赋值。
InnerClass属性:记录内部类与宿主类之间的关联;如果一个类中定义了内部类,那编译器将会为它以及所包含的内部类生成InnerClass属性。
Deprecated以及Synthetic属性:
Deprecated:表示不推荐使用。
Synthetic:代表此字段或者方法并不是由Java源码产生的,而是编译器自行添加的。
StackMapTable属性:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。该验证器在JDK1.6中首次使用,并在JDK1.7中强制代替原本基于类型推断的字节码验证器。
signatrue:在JDK1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或者参数化类型,则signature属性会为它记录泛型签名信息。
BootstrapMethods属性:用于保存invokeddynamic指令引用的引导方法限定符。