代码编译的结果:从本地机器码转变为字节码
一、概述
越来越多的程序语言选择了与【操作系统】和【机器指令集】无关的、平台中立的格式作为程序【编译】后的【存储格式】
"与平台无关"的理想最终实现在操作系统的【应用层】上:
Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了 程序的"一次编写,到处运行"。
①. 各种不同平台的虚拟机与所有平台都统一使用的【程序存储格式】——字节码(Byte Code)是构成【平台无关性】的基石。
②. 虚拟机的另外一种中立特性——【语言无关性】正越来越被开发者所重视。
让【其他语言】运行在【Java虚拟机】上。
③. 实现【语言无关性】的基础任然是【虚拟机】和【字节码存储格式】
Java虚拟机不和包括Java在内的任何语言绑定,它只与"Class文件"这种特定的【二进制文件格式】所关联。
Class文件中包含:
①. Java虚拟机指令集
②.符号表
③.其他辅助信息
基于安全方面考虑,Java虚拟机规范要求在Class文件中使用许多【强制性的语法】和【结构化约束】,但任一门功能性语言都可以表示为一个能 被Java虚拟机所接受的有效的Class文件。
作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。
例如:
使用Java编译器可以把Java代码编译为【存储字节码】的Class文件,
使用JRuby等其他语言的编译器一样可以把程序代码编译成【Class文件】,【虚拟机】并不关心【Class的来源】是何种语言
④. Java语言中的各种【变量】、【关键字】、【运算符号】的语义最终都是由多条【字节码命令】组合而成的
因此【字节码命令】所能提供的语义描述能力会比Java语言本身更加强大。
二、Class类文件的结构(8位字节的二进制流)
任何一个【Class文件】都对应着唯一 一个【类】或【接口】的定义信息
但反过来说,类或接口并不一定都得定义在文件里(类或接口也可以通过【类加载器】直接生成)
本章中,只是通俗地将任意一个有效的【类】或【接口】所应当满足的格式称为"Class文件格式",实际上它并不一定以【磁盘文件】的形式存在。
①. Class文件是一组以【8位字节】为基础单位的【二进制流】,各个数据项严格按照【顺序】紧凑的排列在Class文件中,中间没有添加任何分隔符,没有空隙存在。
②. 当遇到需要占用8位字节以上空间的数据项时,按照【高位在前】的方式【分隔】成若干个8位字节进行存储。
③. Class文件中只有2种【数据类型】:
1. 无符号数(基本数据类型)
无符号数可以用来描述:
【数字】、【索引引用】、【数量值】、【按照UTF-8编码构成的字符串值】
2. 表(多个无符号数或者其他表构成的【复合数据类型】)
所有的表都以"_info"结尾
表用于描述有【层次关系】的【复合结构】数据
整个Class文件本质上就是一张表
④. Class的结构不像XML等描述语言,由于它没有任何【分隔符号】,所以其【顺序】【数量】,甚至数据存储的【字节序】(Byte Ordering , Class 文件中字节序为Big-Endian),都是被严格限定的。
哪个字节代表什么意义,长度多少,先后顺序如何,都不允许改变。
探寻Class文件的结构?
魔数->Class文件主次版本号->常量池->访问标志->类/父类/接口索引集合->字段表集合->方法表集合->属性表集合
1. 魔数
①. 每个Class文件的头4个字节称为"魔数",它唯一的作用就是:
确定这个文件是否为一个能被虚拟机接受的Class文件
注:很多【文件存储标准】中都使用魔数来进行【身份识别】,譬如图片格式(gif,jpeg)等文件头中都存在魔数。
使用【魔数】而不是【扩展名】来进行识别主要是基于【安全方面】考虑,因为文件扩展名可以随意地改动。
文件格式的制定者可以自由的选择魔数值,只要该魔数值没有被广泛采用过同时又不会引起混淆即可。
②. Class文件的魔数是【0xCAFEBABE(咖啡宝贝)】
2.Class文件的版本
①. 紧接着魔数的4个字节存储的是Class文件的版本号
第5、6个字节是【次版本(Minor Version)】
第7、8个字节是【主版本(Major Version)】
②.Java的主版本号是从【45(JDK1.1)】开始的
JDK1.1之后的每个JDK大版本发布【主版本号】向上【加1】
高版本的JDK能向下兼容低版本的Class文件
但低版本JDK不能运行高版本编译后的Class文件
虚拟机也拒绝执行超过其版本号的Class文件
3. 常量池(Class文件的资源仓库)【第9个字节是:常量池容量计数器】
【字面量、符号引用】
紧接着【主次版本】之后的是【常量池入口】
常量池可以理解为:Class文件之中的【资源仓库】
常量池特点:
①. Class文件中与其他项目关联最多的数据类型
②. 占用Class文件空间最大的数据项目
③. Class文件中第一个出现表类型数据的项目
由于【常量池】中【常量】的【数量不固定】,所以在【常量池入口】需要放置一项u2类型的数据,代表常量池【容量计数值(constant_pool_count)】
这个容量计数是从【1】开始的
常量池容量计数器在Class文件的第9个字节位置,十六进行表示
常量池中主要存放2大类常量:
①. 字面量(常量)
字面量比较接近于Java语言层面的【常量】概念
如:文本字符串、声明为final的常量值等
②. 符号引用(类和接口全限定名、字段/方法的名称和描述)
符号引用属于【编译原理】方面的概念,包含3类常量:
1. 类和接口的全限定名
2. 字段的名称和描述
3. 方法的名称和描述
常量池中每一项常量都是一个表
Javap:分析Class文件字节码工具
语法:javap -verbose class文件名
javap命令用于:输出常量表
4. 访问标志(类或接口的访问信息)
在常量池结束之后,紧接着的【2个字节】代表【访问标志access_flags】
访问标志用于标志一些:【类】或【接口层次】的访问信息,包括:
1. 这个【Class】是【类】还是【接口】
2. 是否定义为public类型
3. 是否定义为abstract类型
4. 如果是【类】的话,是否被声明为final等
5. 类索引(类的全限定名)、父类索引(父类的全限定名)、接口索引集合(类实现了哪些接口)
Class文件中由这三项数据来确定这个类的继承关系
1. 类索引:用于确定这个类的【全限定名】
2. 父类索引:用于确定这个类的【父类】的【全限定名】
Java语言不允许【多重继承】,所以【父类索引】只有【一个】
除了java.lang.Object之外,所有的Java类都有父类。
因此除了java.lang.Object外,所有Java类的父类索引都【不为0】
3. 接口索引集合:用于描述类实现了哪些【接口】
这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序【从左到右】排列在接口索引集合中
类索引、父类索引、接口索引集合都按【顺序】排列在【访问标志】之后
6. 字段表集合(类或接口中声明的变量)
字段表集合用于描述【接口】或【类】中声明的【变量】
字段(field)分为:
①. 类级变量(static修饰)
②. 实例级变量
不包括在【方法内部】声明的【局部变量】
Java中描述一个字段可以包含的信息:
1. 字段的作用域(public、private、protected修饰符、默认)
2. 是【实例变量】还是【类变量】(static修饰符)
3. 可变性(final)
4. 并发可见性(volatile修饰符,是否强制从主内存读写)
5. 可否被序列化(transient修饰符)
6. 字段数据类型(基本类型、对象、数组)
7. 字段名称
上述信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示
而字段被定义为什么数据类型、字段叫什么名字是无法固定的,只能引用【常量池】中的【常量】来描述
全限定名:
例如:org/fenixsoft/clazz/TestClass 是这个类的全限定名,仅仅是把类全名中的"."替换成了"/"
多个全限定名最后加入一个";",表示全限定名结束
简单名称:
简单名称是指:【没有类型】和【参数修饰】的【方法】或者【字段】【名称】
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}
这个类中的inc()方法和m字段的简单名称分别是"inc"和"m"
描述符:
描述符的作用是:描述字段的数据类型、方法的参数列表(数量、类型、顺序)、返回值
基本数据类型以及代表无返回值的void类型都用一个【大写字符】表示
特别注意:
J:基本类型long
Z:基本类型boolean
V:特殊类型void
L:对象类型,如 : Ljava/lang/Object
【对象类型】用【字符L】加【对象全限定名】来表示
数组类型:
每一维度将使用一个前置的"["字符来描述
一维数组:一个整形数组"int[]"将被记录为:"[I"
二维数组:一个定义为"java.lang.String[][]"类型的二维数组,将被记录为:"[[Ljava/lang/String"
描述符描述方法时,按照先【参数列表】,后【返回值】的顺序描述
【参数列表】按照参数的严格顺序放在一组小括号"()"内
如方法: void inc()
描述符为:"()V"
方法:java.lang.String toString()的描述符为:"()Ljava/lang/String;"
方法:int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符为:“([CII[CIII)I”
【字段表集合】中不会列出从【超类】或者【父接口】中继承而来的字段,但有可能列出原本Java代码之中不存在的字段
譬如:内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
在Java语言中字段是【无法重载】的
两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称
但对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的
7.方法表集合
方法里面的代码去哪里了?
方法里面的Java代码,经过【编译器】【编译】成【字节码指令】后
存放在【方法属性表集合】中的一个名为"Code"的属性里面
【属性表】作为Class文件格式中最具有扩展性的一种数据项目
如果父类方法在子类中没有被【重写(Override)】,方法表集合中就不会出现来自父类的方法信息
但同样的,有可能会出现由【编译器】自动添加的方法
最典型的便是【类构造器<clinit>】方法和【实例构造器<init>】方法
在Java语言中,要【重载(overload)】一个方法,除了要与原方法具有相同的简单名称外,还要必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅靠返回值的不同来对一个已有方法进行重载的。
但是在Class文件格式中,特征签名的范围要更大一些,只要描述符不是完全一致的两个方法也可以共存。
也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
8. 属性表集合
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。