我们在前面的几篇文章已经提到过很多次,Java是一种平台无关性的语言,而平台无关性主要是因为有了中间代码—字节码。其运行流程如下所示:
同样的,我们的
JVM 有着更强大的功能,平台无关性和语言无关性,如下图所示,无论什么语言,只要编译后是符合 Java 虚拟机规范的字节码文件, JVM 便可以正常运行。
那
Class 文件究竟是什么样子的一个格式,什么一个结构的呢?下面让我们一起来解开它的面纱。字节码具有以下结构特征
1、 Class文件是以8bit为基础单位的二进制数据流,也就是我们的一个Byte。
2、 字节码数据严密紧凑,无分隔符,这一点是效仿当初的C语言。
3、 字节码只存在无符号数字和表两周数据类型
4、 所有的字节码数据采用UTF-8字符编码
学过数据结构的都知道,任何物质都是有数据构成的,字节码主要有无符号数字和表构成,无符号数字使用u1、u2、u3、u4表示一个、两个、三个和四个字节,用于描述数字、索引、数值和字符串等,而表主要以无符号数和其他表组合而成的复合数据,通常 以_info结尾。
那字节码为减是如何生成的呢?这一点我在上一篇博文中已经写的很详细了,大家可以参考上篇博文。
类在加载前必须先解析字节码,字节码主要由以下文件格式构成
名称 | 类型 | 数量 |
magic | u4 | 1 |
minor_version | u2 | 1 |
major_version | u2 | 1 |
constant_pool_count | u2 | 1 |
constant_pool | cp_info | constant_pool_count-1 |
access_flags | u2 | 1 |
this_class | u2 | 1 |
super_class | u2 | 1 |
interfaces_count | u2 | 1 |
interfaces | u2 | interfaces_count |
fields_count | u2 | 1 |
fields | field_info | fields_count |
methods_count | u2 | 1 |
methods | method_info | methods_count |
attributes_count | u2 | 1 |
attributes | attribute_info | attributes_count |
所有的字节码文件都以0xCAFEBABE打头,这个被成为是魔数。当年Java刚出来的时候因为咖啡比较火,开发JVM的人员就希望Java有一天和咖啡一样流行,就在字节码的的开头打上了cafe的关键字,并把Java的logo都设计成了咖啡的样子。其实很多文件都是有一个固定的开头的,如jpeg的文件的打头字节码为0xFFD8FFE0。我们使用UE等编辑软件打开即可看到,如下图所示:
接下分别是编译这段字节码的编译器的小版本和大版本,如下图所示:
对应的版本关系如下(注意,这个是
16 进制的哈) JDK 编译器版本 | target 参数 | 十六进制minor.major | 十进制minor.major |
jdk1.1.8 | 不能带 target 参数 | 00 03 00 2D | 45.3 |
jdk1.2.2 | 不带(默认为 -target 1.1) | 00 03 00 2D | 45.3 |
jdk1.2.2 | -target 1.2 | 00 00 00 2E | 46.0 |
jdk1.3.1_19 | 不带(默认为 -target 1.1) | 00 03 00 2D | 45.3 |
jdk1.3.1_19 | -target 1.3 | 00 00 00 2F | 47.0 |
j2sdk1.4.2_10 | 不带(默认为 -target 1.2) | 00 00 00 2E | 46.0 |
j2sdk1.4.2_10 | -target 1.4 | 00 00 00 30 | 48.0 |
jdk1.5.0_11 | 不带(默认为 -target 1.5) | 00 00 00 31 | 49.0 |
jdk1.5.0_11 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
jdk1.6.0_01 | 不带(默认为 -target 1.6) | 00 00 00 32 | 50.0 |
jdk1.6.0_01 | -target 1.5 | 00 00 00 31 | 49.0 |
jdk1.6.0_01 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
jdk1.7.0 | 不带(默认为 -target 1.6) | 00 00 00 32 | 50.0 |
jdk1.7.0 | -target 1.7 | 00 00 00 33 | 51.0 |
jdk1.7.0 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
这里我们可以看到有一个target参数,如我们看最后一行JDK1.7,但是他的target只有1.4,大版本只有48,这个表示虽然是由JDK1.7编译的,但是需要编译成兼容JDK1.4的版本,生成的字节码是可以在JRE1.4的环境上运行的。
跟着大小编译器版本后面的就是我们的常量池。常量池主要有字面常量和符号引用两种组成。字面常量主要包括文本常量,被声明为final的常量值等。符号引用主要包含类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符等。其中常量池中每一种常量都是使用表来进行描述的。常量中的表如下图所示
其详细的结构如下图所示
常量值最基本的就是
UTF-8 类型的字符串,如下图所示
01
表示 UTF-8 后面跟的是他的内容。我们可以通过 javap –verbose ${ClassName} 来查看。紧接着的两个字节是访问标识符,如下表所示
我们来看一个实例,刚才那段代码对应的访问标识符如下:
对照表格我们可以清楚的看到对应的访问标识符是
public 的。下面紧接着的是对应的类索引、父类索引和接口索引集。如图所示:
这里
0x07 属于类结构,对应的索引为 0x11 ,为第 17 个常量,对应的数据如下图所示
这里我们就可以看待对应的类名了,是一个
UTF-8 类型的字符串。同样的,我们可以查找对应的父类和接口索引。我们也可以同 javap –verbose ${ClassName} 来查看,命令运行的结构如下图所示:
我们可以清晰的看到对应的常量和关系。
紧接着下面的数据是字段表,他的结构如下所示
他有一个访问标识符,字段名称索引,描述索引和属性表。属性表我们稍后在讨论,现在我们来了解以下敌营的描述符。
描述符是通过一个简单的符号来代替一段符号的描述,如Int就是用一个I表示,数组就使用[表示,下面是一些基本的例子。
我们前面已经提到了一个简单名称、全限定名和描述符,那这几个有啥区别的呢?下面用一个例子来展示以下
不用多说,大家一看就明白了吧!具体到我们之前的那段代码示例,如下图所示
接下来的是方法表,方法表和字段表很像,结构如下图所示
对应的访问标识符如下:
结合上面的示例如下:
适应
javap 可以很清楚的看到方法表的内容
在
Java 里面,我们没定义一个方法, JVM 在运行时知道我们要调用哪些方法,那这些是通过什么来区分的呢 ? 有人会说,肯定通过方法名了,那方法名相同的怎么办 ? 也许你继续会说,方法名相同,还有参数不同哇?那返回值 不同行不行?我们很早已经知道答案,方法名相同参数不同的函数写法被称为是重载,在 Java 语言中,重载一个方法除了需要和原方法相同的方法名之外,还需要和原方法有不同的特征签名。而这个特征签名包括顺序的签名类型的集合,并不包含返回值类型,因此不同的返回值类型不能重载,但是我们可以定义返回类型不同、名称相同、签名不同的方法,如下图代码所示。
紧接着方法表,接下来的就是属性表,属性表主要包含以下信息
我们使用
javap 命令可以很详细的看到对应的结果。
JDK
从 1.5 以后加入了 不少新的属性表,下表展示了 JDK1.5 1.6 新加入的属性表。
但是这些字节码被解析之后还需要哪些步骤去执行的呢?请参加下篇博文。字节码执行引擎。