无关性
Java的平台无关性
Java有一句响亮的口号:“一次编写,到处运行”,也就是说Java程序编写一次,就可以运行在任意的硬件系统上。实现这一承诺,离不开Java虚拟机的存在:Java为每种硬件系统编写了不同的虚拟机,帮助运行在虚拟机之上的Java程序屏蔽掉了硬件系统的底层实现,即不同的硬件系统为Java程序的运行提供了环境,因此实现了Java程序可以运行在任意的硬件系统上。
JVM的语言无关性
我们知道,Java程序需要Java虚拟机才能实现运行,然而,Java虚拟机并不只支持Java程序的运行,其他的程序语言,例如JRuby、Jython等也可在Java虚拟机上运行。
无关性的基石
实现上述无关性的基础是以字节码方式存储数据的类文件结构。
Class文件结构概述
●Class文件是二进制文件,它的内容有严格的规范,各个数据项目严格按照规范排列在Class文件里,中间没有空隙存在。
●Class文件中只有两种数据类型:无符号数和表。
- ●无符号数属于基本的数据类型,有u1、u2、u4、u8之分,分别代表1个字节的无符号数、2个字节的无符号数、4个字节的无符号数、8个字节的无符号数。
- ●表是由多个无符号数或其他表作为数据项构成的复合数据结构,所有表都习惯性按“_info”结尾。
●Class类文件的结构组成(严格按照顺序):魔数、版本号、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合。
Class文件组成1:魔数
每个Class文件的头四个字节被称为魔数,作用是确定当前文件是否为Class文件。
Class文件组成2:版本号
魔术过后是版本号,作用是记录当前Class文件的版本,也就是JDK的版本。需要注意的是,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本Class文件,即便文件结构没有发生改变。
Class文件组成3:常量池
常量池的特点
1.版本号过后是常量池入口,常量池可以理解为Class文件中的资源库,是Class文件与其他项目关联最多的数据类型,也是Class文件中空间最大的数据项目之一。
2.常量池中常量的数量不固定,因此在常量池的入口有一个u2类型数据,用作常量池容量计数器。
3.常量池容量计数器的索引是从1而不是0开始,也就是说当计数器值为6时,表示有5个常量。Class文件中只有常量池的容量计数是从1开始的。
4.常量池主要存放两大类常量:字面量和符号引用。
- ●字面量:主要是文本字符串、final修饰的常量等。
- ●符号引用:包括三类常量,分别是类和接口的全限定符、字段的名称和描述符、方法的名称和描述符。
常量池的作用
Java代码在编译成字节码的时候,并不会在Class文件里保存各个方法、字段的最终内存布局信息,而在等类加载器加载Class文件的时候才进行动态的连接,因此这些方法、字段在只有在运行期得到转换才能获取真正的内存入口地址,才能真正被使用。而常量池的作用就是存储虚拟机提供所需的符号引用,这些符号引用在虚拟机运行时被解析到具体的内存地址中。
常量池中的常量
常量池中的常量都是表结构,总共有14种,他们的共同特点是表开始的第一位是u1类型的标志位,表示当前这个常量属于哪种常量类型。
下面以常量CONSTANT_Class_info为例解析常量的结构:
| 类型 | 名称 | 数量 |
|---|---|---|
| u1 | tag | 1 |
| u2 | name_index | 1 |
●tag为标志位,表示当前常量属于14种常量中的哪一种。若该标志位0x07,则说明该常量是CONSTANT_Class_info类型,代表一个类或接口的符号引用。
●name_index是一个索引值,它指示虚拟机按照这个索引值寻找常量池中的某一个常量,而这个常量是CONSTANT_Utf8_info类型,表示UTF-8编码的字符串类型。在这里该常量存储的是类或接口的全限定名。
常量CONSTANT_Utf8_info的类型如下:
| 类型 | 名称 | 数量 |
|---|---|---|
| u1 | tag | 1 |
| u2 | length | 1 |
| u1 | bytes | length |
tag为标志位,若该标志值为0x01,说明该常量是CONSTANT_Utf8_info类型,代表一个长度为length的字符串bytes。
超过64KB大小的变量或方法名不能被编译
Class文件中的字段和方法表都需要引用CONSTANT_Utf8_info来描述名称,而CONSTANT_Utf8_info中的length为u2类型,最大值为65535,表示描述名称的字符串最大为65535字节长,即约为64KB。
Class文件组成4:访问标志
常量池过后是访问标志,该标志用于识别一些类或接口层次的访问信息。
Class文件组成5:类索引、父类索引、接口索引集合。
●类索引和父类索引都是一个u2类型的数据,用于确定类和父类的全限定名。类索引、父类索引这两个值各自指向在常量池中的CONSTANT_Class_info类型常量,通过这个常量中的索引值又可以找到一个CONSTANT_Utf8_info类型的常量,也就是找到存储在CONSTANT_Utf8_info常量里的全限定名字符串。
●接口索引集合是一组u2类型的数据集合,用来描述多个接口的全限定名。
Class文件组成6:字段表集合
字段表的作用
字段表用于描述类或接口中声明的变量。字段包括类级变量(static)、实例级变量,但不包括方法体里声明的局部变量。
字段表结构
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
●access_flags:字段访问标志,通过设置该值来描述字段的信息,例如是否为public、是否为private等。
●name_index:简单名称索引,是对常量池中常量的引用,描述字段的简单名称。
●descriptor_index:字段的描述符,是对常量池中常量的引用。
●attributes_count:属性表集合计数器。
●attributes:属性表,用于描述字段的一些额外的信息,例如字段的值等。
关于描述符的理解
字段表和方法表里都有描述符的存在,他们的作用如下:
-
●对于字段表来说,描述符描述字段的数据类型。
-
●对于方法表来说,描述符描述参数列表、返回类型。
字段表的特点
●字段表不会存储从父类或者父接口继承而来的字段。
●字段表会自行添加一些必要的字段,例如为了支持内部类对外部类的访问,会在内部类中的字段表中添加一个外部类的实例字段。
●Java语言不支持同名字段的存在,而在Class文件里,名称相同而描述符不同的两个字段是可以同时存在的。
Class文件组成7:方法表集合
方法表描述是方法的信息,它的表结构和字段表结构是一致的,参考上文的字段表结构即可。
方法表的特点
●如果父类方法没有在子类中重写,那么方法表集合中就不会有来自父类的方法信息。
●方法表中存在编译器自行添加的方法,例如构造方法。
●Java语言重载的方法需要简单名称相同,特征签名不同(方法参数属于特征签名,方法返回值不属于);而Class文件只要求方法的描述符不相同即可,也就是简单名称和特征签名都相同的方法,只要返回值不同,就可以共存在Class里。
Class文件组成8:属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表集合,用来描述一些额外的信息。其中Code属性是最重要的一个属性:方法里的代码经过编译后变成字节码指令,存放在方法表的Code属性里。