1.概述
write one, run everywhere。
2. 无关性的基石
实现语言无关性的基础是虚拟机和字节码存储格式。Java虚拟机不和任何语言绑定,它只与"class文件"这种特定的二进制文件格式所关联。如下图所示:
3.Class类文件的结构
任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口并不一定都得定义在文件里,也可以通过类加载器直接生成。
注意,这里所称的"Class文件"格式只是为了理解简单,实际上它并不一定以磁盘文件的形式存在。由于它没有任何分隔符号,所以无论是顺序还是数据量,它的数据项都是被严格限定的。
Class文件是一组以8字节为基础单位的二进制流,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据。这种伪机构只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数,无符号数可以用来表述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
表由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以"info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,下图是它的数据项构成。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这一系列连续的某一类型的数据为某一类型的集合。
3.1 魔数与Class文件的版本
每个Class文件的头4个字节称为"魔数",唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。(很多文件存储标准中都使用魔数来进行身份识别,例如gif、jpeg等文件头中都有魔数)。Class文件的魔数值为:0xCAFEBABY。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~JDK1.1使用了45.0~45.3),高版本号能向下兼容,但不能运行以后版本但Class文件。即使文件格式没有发生变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
例如,JDK1.1支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件。而JDK1.2能支持45.0~45.65535的。
下面是一个小demo:
public class TestClass{
private int m;
public int inc() {
return m+1;
}
}
下图是作者使用WinHex打开Class文件的效果。
3.2 常量池
常量池中的常量数量是不固定的,常量池入口需要放置一个u2类型的数据,代表常量池容量计数值。这个容量的计数是从1开始的,例如下图所示:
常量池容量(偏移地址:0x00000008)是0x0016,即十进制的22。表示常量池中有21个常量,索引范围是1~21。第0项常量空出来目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义,把索引值置为0来表示。
Class文件结构中只有常量池的容量从1开始,其他集合类型如接口索引结合、字段表集合、方法表集合等的容量计数都从0开始的。
常量池主要放量大类常量:字面量和符号引用。
字面量:如文本字符串、声明为final的常量值等。
符号引用包括一下三类常量:
类和接口的权限定名
字段的名称和描述符
方法的名称和描述符
常量池中每一项常量都是一个表。1.7之前是11种,1.7及以后为了更好的支持动态语言调用,额外增加了3中(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info)。
这14种表都有一个共同的特点,表开始的第一位是u1类型的标识位,这14种常量类型代表的具体含义如下所示:
JDK的bin目录中,有用于分析Class文件字节码的工具:javap。使用如下图所示:
它帮助我们把整个常量池的21项常量都计算了出来。有些常量没有在代码里出现过,它是自动生成的。
下图是常量池中的14种常量项的结构总表:
3.3 访问标志
常量池结束后,紧接着是2个字节的访问标志,用于识别一些类或者接口层次的访问信息:这个Class是类还是接口;是否定义为public;是否声明为final等。具体标志为如下图:
访问标志一共16个标志位可以使用,当前只定义了8个,没有使用到的标志位要求一律为0。
3.4 类索引、父类索引与接口索引集合
类索引、父类索都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件中由这三项数据来确定这个类的继承关系。除了java.lang.Object外所有的类都有父类,所以只有它的父类索引为0。
类索引、父类索引、接口索引集合都排在访问标志之后,类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info常量中的全限定名字字符串。
下图演示了类索引查找过程:
接口索引集合:入口的第一项u2类型的数据为接口计数器,表示索引的容量。如果该类没有实现任何接口,计数器为0,后面的索引表不占用任何字节。如下图所示:
从偏移地址0x000000F1开始的3个u2类型的值分别是0x0001、0x0003、0x0000,也就是类索引是1,父类索引是3,接口索引集合大小为0。对应前面使用javap查询的常量池,找出对应的类和父类常量如下所示:
3.5 字段表集合
用于描述接口或者类中声明的变量。字段包括类级变量及实例级变量,但不包括在方法内部声明的局部变量。
下图是字段表的最终格式:
字段修饰符放在access_flas中,是一个u2的数据类型,可以设置的标志位和含义如下图:
跟随access_flags标志的是两项索引值:name_index和descriptor_index。都是对常量池对引用,分别代表着字段对简单名称以及字段和方法的描述符。
方法和字段的描述符作用是:描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字符表示,对象类型用字符L加对象全限定名表示,如下图:
对于数组类型,每一个维度使用一个前置的"["字符来描述,如定义为"java.lang.String[][]"被记录为"[[Ljava/lang/String",一个"int[]"被记录为"[I"。
描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序存放在一组小括号"()"之内。如void inc()的描述符为"()V";方法java.lang.String toString的描述符为"()Ljava/lang/String";方法int indexOf(char[] source,int sourceOffset)描述符为"([CI)I"。
3.6方法表集合
方法表和字段表描述很相似,数据项目含义仅在访问标志和属性表集合的可选项中有所区别,如下图所示:
它所有的标志为及取值见下表:
3.7属性表集合
不再要求各个属性具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入字节定义的属性信息,Java虚拟机会忽略掉它不认识的属性。预定义属性已20多项如下表:
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是完全自定义的,只需要通过u4的长度属性说明属性值所占用的位数即可:
1.Code属性
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为"Code",它代表了该属性的属性名称。
attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共6字节,所以属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表操作数栈深度的最大值。
max_locals局部变量表所需的存储空间。
code_length和code用来存储Java源程序编译后生成的字节码长度和指令。
每个指令占u1类型的单字节,u1类型的取值范围是0x00~0xFF,对应十进制0~255,就是一共可以表达256条指令。
code_length虽然是一共u4类型的长度值,但虚拟机规范中限制了一个方法不允许超过65535条字节码指令,实际上它只使用了u2的长度。
字节码指令之后的是这个方法的显示异常处理表集合。它不是必须存在的。
异常表格式如下图所示:
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。
下图是一段演示异常表如何运作的例子。
public int inc(){
int x;
try {
x = 1;
return x;
} catch (Exception e){
x = 2;
return x;
} finally {
x = 3;
}
}
结合下面两个图看:
下面是对字节码的执行过程的分析:
字节码0~34行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中。如果没有异常出现,会继续走5~9行,将变量x赋值为3,之前保存的Slot的整数1读入到操作栈顶,最后Slot指令会以int形式返回栈顶中的值,方法结束。如果出现了异常,PC寄存器指针转到第10行,第10~20行是将2赋值给变量x,然后将变量x此时的值给Slot,最后在将变量x的值改为3。方法返回前同样将Slot中保留的整数2读到了栈顶。第21行开始,是变量x的值赋为3,并将栈顶的异常抛出,方法结束。
2.Exceptions属性
与前面异常不一样,Exceptions属性的作用是列举出方法中可能抛出的受检查异常。也就是方法描述时在throws关键字后面列举的异常。结构如下表:
3.LineNumberTable属性
它用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果取消生成,程序运行产生异常时,堆栈中不会显示出错的行号;调试的时候也无法按照源码行设置断点。结构如下:
4.LocalVariableTable属性
它用于描述栈帧中局部便链表的变量与Java源码中定义的变量之间的关系。可以在Javac中分别使用-g:none或-g:vars选项取消或要求生成这项信息。如果没有生成,当其他人引用这个方法时,所有的参数名称都会丢失。,IDE将会使用arg0,arg1之类的占位符代替原有的参数名。结构表如下:
其中,local_variable_info项目代表了栈帧与源码中的局部变量的关联,结构表如下:
JDK1.5引入范型后,增加了一个LocalVariableTypeTable属性,它和LocalVariableTable类似,仅仅是把记录的字段描述符descriptor_index替换成了字段的特征签名。对于非泛型类型来说,描述符和特征签名描述的信息基本一致,泛型引入后,描述符中泛型的参数化类型被擦除,描述符就不能准确的描述泛型类型了,因此出现了LocalVariableTypeTable。
5.SourceFile属性
用于记录生成这个Class文件的源码文件名称。结构表如下:
6.Constantvalue属性
作用是通知虚拟机自动为静态变量赋值。对于非static类型的变量的赋值是在实例构造器
7.InnerClass属性
用于记录内部类与宿主类之间的关联。结构表如下:
其中inner_classes_info表的结构如下:
inner_name_index指向常量池中的CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,则这项值为0。
inner_class_access_flags是内部类的访问标志,类似于类的access_flags,取值范围如下:
8.Deprecated及Synthetic属性
两个属性都属于标志类型的布尔属性,Deprecated用于表示某个类、字段或方法,被程序作者定为不推荐使用;Synthetic代表此字段或者方法并不是由Java源码直接产生的。他们的结构表如下:
9.StackMapTable属性
是一个复杂的变长属性,它会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的是代替以前笔记消耗性能的基于数据流分析的类型推导验证器。结构表如下:
10.Signature属性
JDK1.5后增加到Class文件规范中,它是一个可选的定长属性,可以出现在类、属性表和方法表结构的属性表中。任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。
之所以用专门的属性记录泛型类型,因为Java的泛型采用的是擦除法实现的伪泛型,在字节码中,泛型信息编译(类型变量、参数化类型)后都被擦除掉。结构表如下:
11.BootstrapMethod属性
在JDK1.7发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于类文件的属性表中,它保存了invokedynamic指令引用的引导方法限定符。结构表如下:
其中bootstrap_method属性结构表如下:
4.字节码指令简介
Java虚拟机的指令由一个字节长度的、代表某种特定操作含义的数字(操作码)以及跟随其后的0个或者多个代表此操作所需参数(操作数)构成。由于Java虚拟机采用面向操作数栈而不是寄存器的加够,所以大多数的指令都不包含操作数。
字节码指令集:由于Java虚拟机操作码的长度为一个字节,所以指令集的操作码总数不能超过256条;Class文件格式放弃了编译后代码的操作数长度对齐,所以虚拟机处理那些超过一个字节数据的时候,需要重建出具体数据的结构。
例如将一个16位长度的无符号整数使用两个无符号字节存储起来,值是:
(byte1 << 8) | byte2
这种操作会导致解释执行字节码时损失一些性能。好处是它放弃了操作数长度对齐,意味着可以省略很多填充和间隔符号;用一个字节代表操作码,可以尽可能获得短小精干的编译代码。
下面是Java虚拟机解释器的伪代码:
4.1 字节码与数据类型
大多数的指令都包含了其操作对应的数据类型信息。礼服iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的是float类型的数据。但是这两条指令在虚拟机内部可能是同一段代码来实现的,只不过它们必须拥有独立的操作码。
4.2 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。这类指令包括如下内容:
将一个局部变量加载到操作栈:iload、i load
、lload、lload 、fload、fload 、dload、dload 、aload、aload 。 将一个数值从操作数栈存储到局部变量表:istore、istore
、lstore、lstore 、fstore、fstore 、dstore、dstore 、astore、astore 。 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_
、fconst_ 、dconst_ 。 扩充局部变量表到访问索引的指令:wide。
4.3 运算指令
运算指令用于对两个操作数栈上的值进行特定运算,并把结果重新存入到操作栈顶。
大体上分两种:整型数据进行运算的指令和浮点型数据运算的指令。无论哪种,都没有直接支持byte、short、char、boolean类型的算术指令,它们都使用操作int类型的指令代替。
4.4 类型转换指令
可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
4.5 对象创建与访问指令
创建类实例的指令:new。
创建数组的指令:newarray、anewarray、multianewarrary
访问类字段(static字段)和实例字段(非static字段)的指令:getfield、putfield、getstatic、putstatic。
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。
4.6 操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样。
4.7 控制转移指令
让Java虚拟机有条件或无条件地从指定的位置指令继续执行程序,而不是从控制转移指令的下一条继续执行,概念模型上说,控制转移指令就是在有条件或无条件地修改PC寄存器的值。
4.8 方法调用和返回指令
方法调用:分派、执行过程第8章详解。
4.9 异常处理指令
4.10 同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用管程支持。
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。如果被设置了,执行线程就会先持有管程,方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,其他任何线程无法再获取同一个管程。
正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持。譬如如下代码:
void onlyMe (Foo f) {
synchronized (f){
doSomething();
}
}
编译后,字节码序列如下:
5.公有设计与私有实现
只要符合Java虚拟机规范,虚拟机以任何方式实现这些定义都是可以的,虚拟机后台如何处理Class文件完全是实现者自己的事情,只要它在外部接口上看起来与规范描述的一致。