ASM指南翻译-9

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


ASM指南翻译-9

3.2 getF方法帧的状态:a)初始化,b)ALOAD 0以后,cGETFIELD以后

 

 

Setter方法的字节码如下:

ALOAD 0

ILOAD 1

PUTFIELD pkg/Bean f I

RETURN

 

第一条指令将this放置到操作数栈栈顶。第二条指令将索引为1的局部变量值放置到栈顶,这个索引的值为方法的参数,在方法调用创建帧的过程中初始化的。第三个指令从栈顶弹出这两个值,并将int值存贮到this对象的字段f中,this.f。最后一条指令,在源代码中是隐式定义的,但是在编译后的代码中是必须的,它负责销毁执行帧并将调用返回给调用者。这个方法的执行帧的状态如图3.3


ASM指南翻译-9

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的局部变量放置到操作数栈栈顶,这个局部变量就是方法参数fIFLT指令从栈顶弹出这个值,然后将它和0比较,如果结果小于0LT),执行就跳转到label表示的指令处接着执行,否则不做任何事,接着往下面执行。接下来的三条指令与之前setF中的指令相同。GOTO指令无条件地跳转到end表示的指令处,也即是RETURN指令。在labelend标签之间的指令,创建并且抛出一个异常:NEW指令创建异常对象,并将它放置到栈顶,DUP指令复制栈顶元素,并重新放置到栈顶。INVOKESPECIAL指令从栈顶取得异常的一个拷贝,然后调用这个异常对象的构造方法。最后,ATHROW指令弹出剩余的异常对象拷贝,然后抛出(这样执行流程就不会传递到下一条指令)。

 

3.1.4异常处理

没有字节码指令用于捕获异常:只有与方法关联的异常处理程序(exception handler)列表,它们指定了在方法执行的某部分发生异常时应该执行的代码。一个异常处理程序类似于try catch块:它有一个范围,就是try包含的块所对应的指令序列,异常处理程序就对应着catch块内容。范围是由startend标签指定,异常处理程序以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

 

trycatch之间的指令就对应着源码中的try代码块,catch后面的指令就对应着源码中的catch代码块。TRYCATCHBLOCK行指定了一个异常处理程序,覆盖的范围就是trycatch之间的代码,对应的处理程序从catch标签开始,对应的异常是InterruptedException的子类。这意味着在trycatch之间的任何地方发生了这样的异常,栈将被清空,异常就被放入这个空栈中,执行流程就传递到catch指令。

 

3.1.5

使用java6或者更高版本的jdk编译的类,除了包含字节码指令之外,还包含一个栈映射帧(stack map frames)集合,它们用来加速虚拟机内部的类验证流程。一个栈映射帧描述了在一个方法执行时,在一些点的执行帧状态。更精确地说法就是,它给出了在字节码指令执行前,局部变量区和操作数栈区每个值的类型(type)。

 

例如,我们来考虑之前的getF方法,我们可以定义三个栈映射帧来分别描述ALOAD,GETFIELDIRETURN这三个指令之前的执行帧状态。这三个栈映射帧对应了三种情形,见图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

你可能感兴趣的:(ASM)