概述
- 字节码文件的跨平台性:
~~~~~~~~ i. Java语言:跨平台的语言
~~~~~~~~ ii. Java虚拟机:跨语言的平台。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。 - 想要一个Java程序正确运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码
~~~~~~~~ i. 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件
~~~~~~~~ ii. javac是一种能够将Java源码编译为字节码的前端编译器
~~~~~~~~ iii. javac编译器在将Java源码编译为一个有效的字节码文件的过程中经历了4个步骤:词法解析、语法解析、语义解析以及生成字节码
~~~~~~~~
Class文件结构
-
Class类的本质:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class文件是一组以8位字节为基础单位的二进制流。
-
Class文件格式:
~~~~~~~~ i. Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义、长度是多少、先后顺序如何,都不允许改变。
~~~~~~~~ ii. Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。
~~~~~~~~ ~~~~~~~~ 1) 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
~~~~~~~~ ~~~~~~~~ 2) 表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以 _info 结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。 -
Class文件结构概述:Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
1. Class文件的标识:魔数
~~~~~~~~
i. 每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)
~~~~~~~~
ii. 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符
~~~~~~~~
iii. 魔数值固定为0xCAFEBABE,不会改变
~~~~~~~~
iv. 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出如下错误:
~~~~~~~~
v. 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动
2. Class文件版本号
~~~~~~~~
i. 紧接着魔数的4个字节存储的是Class文件的版本号,同样也是4个字节。第5和第6个字节所代表的含义就是编译的副版本号minor_version,而第7和第8个字节就是编译的主版本号major_version
~~~~~~~~
ii. 它们共同构成了Class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m
~~~~~~~~
iii. 不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常
3. 常量池:存放所有常量
~~~~~~~~
i. 常量池是Class文件中内容最为丰富的区域之一,是整个Class文件的基石
~~~~~~~~
ii. 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项
~~~~~~~~
iii. 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型无符号数,代表常量池容量计数值(constant_pool_cout)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的
~~~~~~~~
iv. Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合
~~~~~~~~
v. 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
~~~~~~~~
~~~~~~~~
1) 字面量:
~~~~~~~~
~~~~~~~~
~~~~~~~~
a) 文本字符串
~~~~~~~~
~~~~~~~~
~~~~~~~~
b) 声明为final的常量值
~~~~~~~~
~~~~~~~~
2) 符号引用:
~~~~~~~~
~~~~~~~~
~~~~~~~~
a) 类和接口的全限定名
~~~~~~~~
~~~~~~~~
~~~~~~~~
b) 字段的名称和描述符
~~~~~~~~
~~~~~~~~
~~~~~~~~
c) 方法的名称和描述符
~~~~~~~~
vi. Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译导具体的内存地址中。
4. 访问标识
在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类的话,是否被声明为final等
5. 类索引、父类索引、接口索引集合
~~~~~~~~
i. 在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:
~~~~~~~~
~~~~~~~~
ii. 这三项数据来确定这个类的继承关系:
~~~~~~~~
~~~~~~~~
1) 类索引用于确定这个类的全限定名
~~~~~~~~
~~~~~~~~
2) 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0
~~~~~~~~
~~~~~~~~
3) 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中
6. 字段表集合
~~~~~~~~
i. 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量
~~~~~~~~
ii. 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
~~~~~~~~
iii. 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public/private/protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等
7. 方法表集合
指向常量池索引集合,它完整描述了每个方法的签名
8. 属性表集合
~~~~~~~~
i. 方法表集合之后的属性表集合,指的是Class文件所携带的辅助信息,比如该Class文件的源文件的名称,以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的测试,一般无须深入了解
~~~~~~~~
ii. 此外,字段表、方法表都可以有自己的属性表,用于描述某些场景专有的信息
~~~~~~~~
iii. 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略它不认识的属性