3.1.3示例
接下来我们看看一些基本的例子,以获取关于字节码指令如何工作的具体印象。考虑下面这个Bean类:
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
其中getter方法的字节码为:
ALOAD 0
GETFIELD pkg/Bean f I
IRETURN
第一条指令读取索引位置为0的局部变量this,这个局部变量是在这个方法调用时创建的帧的过程中被初始化的,然后将这个局部变量放置到操作数栈栈顶。第二条指令从栈顶弹出这个值,this,然后将字段f的值放置到栈顶,this.f。最后一条指令从栈顶弹出得到的字段f的值,将它返回给调用者。这个方法执行过程中帧的状态如图3.2:
图3.2 getF方法帧的状态:a)初始化,b)ALOAD 0以后,c)GETFIELD以后
Setter方法的字节码如下:
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
RETURN
第一条指令将this放置到操作数栈栈顶。第二条指令将索引为1的局部变量值放置到栈顶,这个索引的值为方法的参数,在方法调用创建帧的过程中初始化的。第三个指令从栈顶弹出这两个值,并将int值存贮到this对象的字段f中,this.f。最后一条指令,在源代码中是隐式定义的,但是在编译后的代码中是必须的,它负责销毁执行帧并将调用返回给调用者。这个方法的执行帧的状态如图3.3:
图3.3 setF方法执行帧的连续状态:a)初始化,b)ALOAD 0之后,
c)ILOAD 1以后,d)PUTFIELD以后
这个Bean类也有一个缺省的共有的构造方法,它是由编译器生成的,因为程序员没有显示的定义构造方法。缺省的构造方法的代码是 Bean(){ super();}。它的字节码如下:
ALOAD 0
INVOKESPECIAL java/lang/Object <init> ()V
RETURN
第一条指令将this放置到操作数栈栈顶。第二个指令从栈顶弹出这个值,然后调用定义在Object类中的<init>方法。这对应着super()方法调用,就是调用父类Object的构造方法。在这里可以看出这里的名称在源代码和编译后的代码中是不一样的:在编译后的类中一直为<init>,而在源代码中它们的名称和类名一样。最后一条指令返回到方法调用者。
下面让我们考虑一个更复杂的setter方法:
public void checkAndSetF(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}
这个setter方法的字节码如下:
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
ATHROW
end:
RETURN
第一条指令将索引为1的局部变量放置到操作数栈栈顶,这个局部变量就是方法参数f。IFLT指令从栈顶弹出这个值,然后将它和0比较,如果结果小于0(LT),执行就跳转到label表示的指令处接着执行,否则不做任何事,接着往下面执行。接下来的三条指令与之前setF中的指令相同。GOTO指令无条件地跳转到end表示的指令处,也即是RETURN指令。在label和end标签之间的指令,创建并且抛出一个异常:NEW指令创建异常对象,并将它放置到栈顶,DUP指令复制栈顶元素,并重新放置到栈顶。INVOKESPECIAL指令从栈顶取得异常的一个拷贝,然后调用这个异常对象的构造方法。最后,ATHROW指令弹出剩余的异常对象拷贝,然后抛出(这样执行流程就不会传递到下一条指令)。
3.1.4异常处理
没有字节码指令用于捕获异常:只有与方法关联的异常处理程序(exception handler)列表,它们指定了在方法执行的某部分发生异常时应该执行的代码。一个异常处理程序类似于try catch块:它有一个范围,就是try包含的块所对应的指令序列,异常处理程序就对应着catch块内容。范围是由start和end标签指定,异常处理程序以start开始。示例代码如下:
public static void sleep(long d) {
try {
Thread.sleep(d);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
编译之后:
TRYCATCHBLOCK try catch catch java/lang/InterruptedException
try:
LLOAD 0
NVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN
在try和catch之间的指令就对应着源码中的try代码块,catch后面的指令就对应着源码中的catch代码块。TRYCATCHBLOCK行指定了一个异常处理程序,覆盖的范围就是try和catch之间的代码,对应的处理程序从catch标签开始,对应的异常是InterruptedException的子类。这意味着在try和catch之间的任何地方发生了这样的异常,栈将被清空,异常就被放入这个空栈中,执行流程就传递到catch指令。
3.1.5帧
使用java6或者更高版本的jdk编译的类,除了包含字节码指令之外,还包含一个栈映射帧(stack map frames)集合,它们用来加速虚拟机内部的类验证流程。一个栈映射帧描述了在一个方法执行时,在一些点的执行帧状态。更精确地说法就是,它给出了在字节码指令执行前,局部变量区和操作数栈区每个值的类型(type)。
例如,我们来考虑之前的getF方法,我们可以定义三个栈映射帧来分别描述ALOAD,GETFIELD和IRETURN这三个指令之前的执行帧状态。这三个栈映射帧对应了三种情形,见图3.2,可以如下描述,第一个括号中描述的是局部变量的类型,其它的则是操作数栈中数据的类型:
State of the execution frame before Instruction
[pkg/Bean] [] ALOAD 0
[pkg/Bean] [pkg/Bean] GETFIELD
[pkg/Bean] [I] IRETURN
我们也可以针对checkAndSetF方法进行类似的操作:
[pkg/Bean I] [] ILOAD 1
[pkg/Bean I] [I] IFLT label
[pkg/Bean I] [] ALOAD 0
[pkg/Bean I] [pkg/Bean] ILOAD 1
[pkg/Bean I] [pkg/Bean I] PUTFIELD
[pkg/Bean I] [] GOTO end
[pkg/Bean I] [] label:
[pkg/Bean I] [] NEW
[pkg/Bean I] [Uninitialized(label)] DUP
[pkg/Bean I] [Uninitialized(label) Uninitialized(label)] INVOKESPECIAL
[pkg/Bean I] [java/lang/IllegalArgumentException] ATHROW
[pkg/Bean I] [] end:
[pkg/Bean I] [] RETURN
除了Uninitialized(label)类型之外,这个和之前的方法没什么区别。这个Uninitialized(label)类型是一个特殊的类型,只用在栈映射帧中,它表示一个对象的内存已经分配,但是其构造方法还未被调用。参数表示创建这个对象的指令。对于这个值所能调用的唯一的方法就是构造方法。当构造方法调用以后,所有的Uninitialized(label)就会被替换为真实的类型,在这里就是IllegalArgumentException。栈映射帧还可以使用另外的三个特殊类型:UNINITIALIZED_THIS是构造方法中索引值为0 的局部变量的初始化类型,TOP对应着未定义的值,NULL对应着null。
就如上面所说,从java6开始,编译的类中包含栈映射帧的集合。为了节省空间,一个编译的方法并不是每个指令都对应着一个帧:事实上只有那些对应着判断指令的目标或者异常处理程序,或者无条件跳转指令才有帧。因为其它的帧可以很容易地很快地从这些帧继承而来。
在checkAndSetF方法的示例中,这意味着只有两个帧被存储:一个是NEW指令,因为它是IFLT指令的目标,同时它紧跟这无条件跳转指令GOTO,另外一个是RETURN指令,因为它是GOTO指令的目标,同时它紧跟着无调教跳转指令ATHROW指令。
为了节省更多的空间,每个帧都会被压缩,然后存储它与之前帧的不同,初始帧不被保存,因为它可以从方法的参数类型推断出来。在checkAndSetF的例子中,那两个必须被保存的帧是和初始帧相同的,因此它们使用F_SAME助记符来存储。这些帧可以在与它们关联的字节码指令之前被助记符表示。下面给出了checkAndSetF方法的最终字节码指令:
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
F_SAME
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
ATHROW
end:
F_SAME
RETURN