方法
3.1 结构
3.1.1 执行模型
每个线程都有自己的执行栈,栈由栈帧组成。每个帧表示一个方法调用。每次调用一个方法时,会将一个新栈帧push入当前线程的执行栈。当方法返回时,会讲这个帧从执行栈中弹出,执行过程在发出调用的方法中继续进行每一帧包括
- 操作数栈
- 局部变量表
- 动态链接(DL)
- 方法返回
局部变量表与操作数栈大小取决于方法的代码。
在创建一个帧时会将其初始化,提供一个空栈,并用目标对象this(非静态方法),以及该方法的参数来初始化局部变量。比如调用a.equals(b)将创建一个帧,它有一个空栈,前两个局部变量被初始化为a和b
3.1.2 字节代码指令
字节代码指令由一个表示该指令的操作码和固定数目的参数组成
- 操作码 是由一个无符号字节值,由助记符号标识。
- 参数时静态值,确定了精确的指令行为
字节代码指令可以分为两类
- 一小组指令。设计用在局部变量表和操作数栈之间传送值
- 仅用于操作数栈:从栈中弹出一些值,根据这些值计算一个结果,并将它压回栈中
ILOAD,LLOAD,FLOAD,DLOAD,ALOAD。 他们的参数必须是读取的局部变量的索引i。
- ILOAD 加载 boolean、byte、char、short、int局部变量
- LLoad、FLoad、DLoad 分别用于记载long,float,double的值
- ALoad 用于加载任意非基元值即对象和数组
ISTORE, LSTORE,FSTORE, DSTORE, ASTORE 从操作数栈中弹出一个值,并且将它存储在由索引i指定的局部变量中
xLoad和xStore指令被赋入了类型。确保不会执行非法转换。实际上,将一个值存储在局部变量中,然后再以不同的类型加载它是非法的。比方说ISTORE 1ALOAD1 序列是非法的。但是向一个局部变量中存储一个值,而这个值的类型不同于该局部变量中存储的当前值,却是完全合法的。
栈
POP弹出栈顶部的值,DUP 压入顶部,SWAP弹出两个值,并且按逆序压入他们
常量
这些指令在操作数栈压入一个常量值:ACONST_NULL 压入null,ICONST_0 压入int0, FCONST_0 压入0f,DCONST_0压入0d,BIPUSH b 压入字节值b,SIPUSH s 压入short值 s,LDC 压入任意int、float、long、double、String或者class 常量cst
算数与逻辑
这些指令从操作数栈弹出数值,合并他们,并将结果压入栈中。他们没有任何参数。xADD,xSUB,xMUL,xDIV和xREM。
类型变换
这些指令从栈中弹出一个值,将其转换为另一种类型,并将结果压入栈中。他们对应Java中的类型转换表达式。I2F,F2D,L2D等将数值由一种数值类型转换为另一种类型。CHECKCAST t将一个引用值类型转换为类型t
对象
这些指令用于创建对象、锁定他们、检测他们的类型,等等。 NEW type 指令将一个type类型的新对象压入栈中
字段
这些指令读或者写一个字段的值。GETFIELD ower name desc 弹出一个对象引用,并将name字段中的值压入栈中。PUTFIELD owner name desc 弹出一个值和一个对象引用,并且将这个值存储在它的name字段中。在这两种情况下,该对象都必须是owner类型,它的字段必须是desc类型。GETSTATIC和PUTSTATIC是类似的指令
方法
这些指令调用一个方法或构造器。它们弹出值的个数等于其方法参数个数 + 1(用于目标对象),并压回方法调用的结果。 INVOKEVIRTUREAL owner name des调用在类owner中定义的name方法,其方法描述符为desc。INVOKESTATIC 用于静态方法,INVOKESPECIAL用于私有方法和构造器。INVOKDYNAMIC用于动态方法调用机制
数组
这些指令用于读写数组中的值。xALOAD指令弹出一个索引和一个数组,并压入此索引处数组元素的值。xASTORE指令弹出一个值,一个索引和一个数组,并将这个值存入在该数组的这一索引处。x可以为I(int)、L(long)、F(float)、D(double)、A(other)、B(byte)、C(char)、S(short)
跳转
这些指令无条件的或者在某一条件为真的时候跳转到任意指令。它们用于编译if、for、do、while、break、continue指令。IFEQ label 从栈中弹出一个int值,如果这个值是0,跳转到label指定的指令处。还有其他的指令IFNE、IFGE、TABLESWITCH、LOOKUPSWITCH 对应switch 指令
返回
RETURN用于返回void方法,xRETURN用于其他方法
3.1.3 举例说明
1 | public class Bean { |
3.1.3.1 getter
getter的字节码为
1 | ALOAD 0 |
读取局部变量0,(局部变量0在为这个方法调用创建帧期间被初始化为this),并且将这个值压入操作数栈中。
从操作数栈中弹出这个值,并且将这个对象的f字段压入栈中
从栈中弹出这个值,返回给调用者
3.1.3.2 setter
setter方法的字节码
1 | ALOAD 0 |
读取局部变量0,(局部变量0在为这个方法调用创建帧期间被初始化为this),并且将这个值压入操作数栈中。
压入局部变量1
弹出这两个值,并将int值存储在被引用对象的f字段中。
销毁当前执行帧,并返回调用者。
3.1.3.3 contractor
bean类有一个默认的构造函数,在没有定义的情况下是由编译器生成的。
1 | Bean() { |
构造函数
1 | ALOAD 1 |
读取局部变量0,(局部变量0在为这个方法调用创建帧期间被初始化为this),并且将这个值压入操作数栈中。
调用Object对象中定义的<init>方法
3.1.3.4 复杂的使用
1 | public void checkAndSetF(int f) { |
1 | ILOAD 1 |
3.1.3.5 异常处理器
1 | public static void sleep(long d) { |
1 | TRYCATCHBLOCK try catch catch java/lang/InterrutedExcetion |
TRYCATCHBLOCK行制定了一个异常处理器, 覆盖了try和catch标记之间的范围,有一个开始于catch标记的处理器,用于处理一些异常,这些异常是InterruptedException的子类。如果在try和catch之间抛出了一个异常,栈将被清空,异常被压入这个空栈中,执行过程在catch中继续。
3.1.3.6 帧
除了字节码指令外,Java6或者更高的编译类中还包括一组栈映射帧,用于加快Java虚拟机中类验证过程的速度。它给出了就要执行某一特定字节码指令之前,每个局部变量槽和每个操作数栈槽总包含的值的类型
为节省空间,已编译方法中没有为每个指令包含一个帧 它仅为那些应对跳转目标或者异常处理器的指令,或者跟在无条件跳转指令之后的指令创建帧。
在checkAndSetF 方法的情景中。仅存储两个帧 一个是用于NEW指令,另一个用于RETURN指令。因为它是GOTO指令的目标,还因为它跟在无条件跳转ATHROW指令后。
3.2 接口和组件
用于生成和转换已编译方法的ASM api 是基于MethodVisitor抽象类的。它由ClassVisitor的visitMethod返回。这些方法必须按照以下顺序调用
visitAnntationDefault?
(visitAnnotation | visitParameterAnnotation | visitAttribute )*
(visitCode
(visitTryCatchBlock | vistLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber) *
visitMaxs)?
visitEnd