Java虚拟机通过系列类加载器加载Class文件,然后读取其中的字节码指令进行工作的。而Class文件则是通过Java编译器编译Java源代码得到的,如下图:
理解编译器是如何与Java虚拟机协同工作的,对编译器开发人员来说很有好处,同样也有助于理解Java虚拟机本身。
下面主要介绍的是Java虚拟机规定的编译规则:
常量、局部变量的使用和控制结构
Java虚拟机是基于栈架构设计的,它的大多数操作是从当前栈帧的操作数栈取出1个或多个操作数,或将结果压入操作数栈中。每调用一个方法,都会创建一个新的栈帧,并创建对应方法所需的操作数栈和局部变量表
前面已经谈到栈和栈帧的概念,在栈帧中存在着操作数栈和局部变量表,两者都被组织为以一个字长为单位、从0开始计数的数组,不同的是前者的访问是通过入栈和出栈,而后者是通过索引完成的。注意在局部变量表中long和double类型占据2个位置,访问时通过第一个位置的索引即可。
下面来看一个小例子:
int a = 100;
int b = 98;
int c = a + b;
我们简单用图表示下上面代码编译后产生的字节码指令在操作数栈和局部变量表中的执行过程:
接着引用书中的例子,先看Java代码:
void spin()
{
int i; for (i = 0; i < 100; i++)
{
; //Loop body is empty
}
}
很简单的一个空循环,再来看一下经过编译后的字节码指令:
Method void spin()
0 iconst_0 //从常量池中取出常量0放入操作数栈1 istore_1 //把0放入局部变量表中第一个位置(i=0)2 goto 8 //第一次不执行i++操作5 iinc 1 1 //(i++)8 iload_1 //从局部变量表中取出第一个局部变量放入操作数栈(i)9 bipush 100 //取出常量 100 放入操作数栈11 if_icmplt 5 //如果满足i<100,执行第五条指令14 return //返回
0和100两个常量分别使用了两条不同的指令压入操作数栈。对于0采用了iconst_0 指令,它属于iconst_指令族。而对于100采用bipush指令。这样的原因是因为Java虚拟机内部规定了入栈的int类型的i是-1、0、1、2、3、4、5时使用iconst_指令,其他的值使用bipush。当然你也可以使用在[-1, 5]时使用bipush指令,但是这样会造成解析的字节码多出一个字节,每次循环时Java虚拟机也会额外的耗费时间去获取和解析这个操作数。
iconst_0这样的形式称作操作码隐式包含。至于为什么不能所有的数值都采用这种格式,我觉得是受限于Java虚拟机操作码的长度。
如果在spin()例子的中循环的计数器使用了非int类型,那么编译代码也要有调整。譬如在spin例子中使用double型取代int,则:
void dspin()
{
double i;
for (i = 0.0; i < 100.0; i++)
{
; //Loop body is empty }
}
相应的自编码也会变成double类型的:
Method void dspin()
0 dconst_0 //从常量池中取出0放入操作数栈1 dstore_1 //从操作数栈取出0放入局部变量表位置1和22 goto 9 //执行95 dload_1 //向操作数栈中放入局部变量1和26 dconst_1 //取出常量1.07 dadd //相加,与int类型不同,double类型没有自增的指令8 dstore_1 //结果放入局部变量表1和29 dload_1 //将局部变量1和2压入操作数栈10 ldc2_w #4 //出去double常量10013 dcmpg //和100进行比较,这里同样没有int类型的直接判断14 iflt 5 //如果小于转向517 return //返回
前文提到过double和long类型的值占用两个局部变量的空间,上例中可以看出来,double在局部变量表中占据了2个位置,访问时使用索引小的访问,这对局部变量不能够分开进行操作。
Java虚拟机中,操作码都为一个字节,这限制了操作码的数量最多是265条。其中对于int类型的操作大部分都可以直接进行,这一部分考虑到操作数栈和局部变量表的实现效率,另一部分也是由于int类型操作的频繁。
在Java虚拟机指令集中,没有对byte,char和short类型变量的store、load和add等指令。例如,用short类型来实现上面spin中的循环时:
void sspin()
{
short i;
for (i = 0; i < 100; i++)
{
; //Loop body is empty }
}
Java虚拟机编译时要将其他可安全转化为int类型的数据转换为int类型进行操作。在将short类型值转换为int类型值时,可以保证short类型值操作后结果一定在int类型的精度范围之内,因此sspin()的编译后代码如下:
Method void sspin()
0 iconst_0
1 istore_1
2 goto 10
5 iload_1 //把short当做int来使用6 iconst_1
7 iadd
8 i2s //int转化为short9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return
在Java虚拟机中,缺乏对byte、short和char类型的相对支持,但是带来的问题并不大。在编译过程中,byte和short类型带符号扩展为int类型,而char零位扩展为int类型。而long和浮点类型,Java虚拟机提供了和int相当的支持,但是没有条件转移指令。
算术运算
Java虚拟机中的算术运算是基于操作数栈来进行的,算术运算中用到的操作数都是从操作数栈中弹出,运算结果再压入操作数栈。在内部运算中ÿ