办法结构

履行模型

每个线程都有自己的虚拟机栈,用于存储栈帧:每逢一个办法被调用,会创立新的栈帧并压入虚拟机栈,办法履行结束后,栈帧从仓库中弹出。

每个栈帧都包括局部变量操作数栈

局部变量表存储对象的this引用、办法参数以及办法内声明的变量,如果是实例办法,第0个局部变量保存this,这以后跟着办法参数。

long和double类型的数据占用两个连续的局部变量,运用较小的索引值定位。

操作数栈是一个后进先出的栈结构,其最大深度编译时确定,栈帧刚初始化时,操作数栈为空。

下面是一个虚拟机栈的示意图:

ASM学习系列2:MethodVisitor

字节码指令

字节码指令由一个指令码和若干个参数组成,如下:

opcode arguments

首要分为两类:

  1. 将局部变量的数据加载到操作数栈
  2. 弹出操作数栈上的数据,履行核算后将成果push到操作数栈中

具体字节码指令请参阅java虚拟机规范:docs.oracle.com/javase/spec…

比如

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

getter办法履行过程栈帧变化内容如下:

ASM学习系列2:MethodVisitor

  • a. 栈帧初始化,局部变量表中仅包括this引用
  • b. 履行ALOAD 0之后,this被加载到操作数栈上
  • c. 履行GETFIELD pkg/Bean f I之后,先将this出栈,在把读到的字段f值入栈

反常处理

反常处理器:即catch块,如下实例:

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
    INVOKESTATIC java/lang/Thread sleep (J)V
    RETURN
catch:
    INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
    RETURN

stack map frames(栈映射帧)

stack map frames是StackMapTable属性的元素,其作用于虚拟机的类型检查验证阶段,首要是加速字节码的验证,它表明在字节码指令履行之前,局部变量表的曹伟和操作数栈中值的类型。

前例中getF办法的stack map frames如下:

State of the execution frame before   Instruction
[pkg/Bean] [] 						ALOAD 0
[pkg/Bean] [pkg/Bean]				 GETFIELD
[pkg/Bean] [I] 						IRETURN

第一个方括号内表明局部变量表的类型,第二个括号内表明操作数栈的类型。

为了节约存储空间,只要以下状况才会保存stack map frames,其他景象能够依据之前的状况推断出当时的map frame:

  • 跳转指令的方针方位
  • 反常处理器
  • 无条件跳转指令的方针方位

在前面Bean类增加如下办法:

public void checkAndSetF(int f) {
    if (f >= 0) {
    	this.f = f;
    } else {
    	throw new IllegalArgumentException();
    }
}

其完整的stack map frames如下:

State of the execution frame before 	  					  Instruction
[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)状况表明已经请求内存,但还未调用结构器。

依据节约存储空间规矩,优化后的stack map frames如下:

    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

因为这段字节码指令中仅有IFLT labelGOTO end两处跳转指令,所以实际上只要保存两个map frame即可,即在label之后和在end之后增加的F_SAME.F_SAME是asm提供的常量,表明具有与前一帧完全相同的局部变量且操作数栈为空。

接口

asm提供MethodVisitor生成或修正办法,接口如下:

abstract class MethodVisitor { // public accessors ommited
    MethodVisitor(int api);
    MethodVisitor(int api, MethodVisitor mv);
    AnnotationVisitor visitAnnotationDefault();
    AnnotationVisitor visitAnnotation(String desc, boolean visible);
    AnnotationVisitor visitParameterAnnotation(int parameter,
    String desc, boolean visible);
    void visitAttribute(Attribute attr);
    void visitCode();
    void visitFrame(int type, int nLocal, Object[] local, int nStack,
    Object[] stack);
    void visitInsn(int opcode);
    void visitIntInsn(int opcode, int operand);
    void visitVarInsn(int opcode, int var);
    void visitTypeInsn(int opcode, String desc);
    void visitFieldInsn(int opc, String owner, String name, String desc);
    void visitMethodInsn(int opc, String owner, String name, String desc);
    void visitInvokeDynamicInsn(String name, String desc, Handle bsm,
    Object... bsmArgs);
    void visitJumpInsn(int opcode, Label label);
    void visitLabel(Label label);
    void visitLdcInsn(Object cst);
    void visitIincInsn(int var, int increment);
    void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);
    void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
    void visitMultiANewArrayInsn(String desc, int dims);
    void visitTryCatchBlock(Label start, Label end, Label handler,
    String type);
    void visitLocalVariable(String name, String desc, String signature,
    Label start, Label end, int index);
    void visitLineNumber(int line, Label start);
    void visitMaxs(int maxStack, int maxLocals);
    void visitEnd();
}

其接口调用顺序如下:

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
    ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
    	visitLocalVariable | visitLineNumber )*
    visitMaxs )?
visitEnd

因而能够用visitCodevisitMaxs检测字节码中办法的开端方位,visitCode代表开端访问办法,visitMaxs表明办法访问结束。

运用MethodVisitor操作字节码时涉及三个组件:

  • ClassReader: 读原始字节码,获取初始MethodVisitor
  • ClassWriter: 获取转化后的MethodVisitor,生成字节码
  • MethodVisitor:完成转化逻辑

ClassWriter选项

ClassWriter结构器接收如下参数:

new ClassWriter(0);
new ClassWriter(ClassWriter.COMPUTE_MAXS)
new ClassWriter(ClassWriter.COMPUTE_FRAMES)
  • 0:栈帧、局部变量和操作数栈的巨细都要手动核算
  • COMPUTE_MAXS:主动核算局部变量和操作数栈的巨细,单仍需调用visitMaxs,可传任意参数,其内部会疏忽参数并重新核算正确的值。仍需手动核算栈帧
  • COMPUTE_FRAMES:栈帧、局部变量和操作数栈的巨细都会主动核算,无需调用visitFrame,但仍要调visitMaxs(参数不重要,内部会主动核算)

COMPUTE_MAXS和COMPUTE_FRAMES用起来更便利,但核算更慢,COMPUTE_MAXS比手动核算更慢(文档写的10%,没有具体测验),COMPUTE_FRAMES又会更慢一些(文档说慢两倍)。

生成办法

考虑前文提到的Bean类:

package pkg;
public class Bean {
    private int f;
    public int getF() {
    	return this.f;
    }
    public void setF(int f) {
    	this.f = f;
    }
    public void checkAndSetF(int f) {
        if (f >= 0) {
            this.f = f;
        } else {
            throw new IllegalArgumentException();
        }
	}
}

getF办法的字节码指令如下:

ALOAD 0
GETFIELD pkg/Bean f I
IRETURN

依据前面剖析其履行过程栈帧变化可知,它的局部变量表长度为1,而且运转过程中操作数栈的最大深度为1,因而能够用如下asm代码生成此字节码:

mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();

checkAndSetF的状况更为杂乱,经过前面的剖析能够知道它的stack map frames如下:

    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

因为checkAndSetF的参数是int f,而且函数内没有声明其他局部变量,因而局部变量表的巨细为2。在为字段f赋值时,需求将this和参数f都加载到操作数栈上,因而操作数栈最大深度为2。其终究的代码如下:

mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();

修正办法

利用MethodVisitor修正办法字节码与ClassVisitor类似:

  • 修正调用参数完成字节码指令的修正
  • 撤销调用对应办法删去指令
  • 增加新的调用来增加对应指令

考虑如下代码,计算并打印C.m()的履行时间:

package com.example.test;
public class C {
    public void m() throws Exception {
        Thread.sleep(100);
    }
}

如果手动修正代码,能够在办法开端和结束的当地增加对应的计算代码,如下:

public class C {
    public static long timer;
    public C() {
    }
    public void m() throws Exception {
        timer -= System.currentTimeMillis();
        Thread.sleep(100L);
        timer += System.currentTimeMillis();
        System.out.println("test m used " + timer + "ms");
    }
}

咱们需求做以下三件事情来主动生成上面的代码:

  1. 增加long类型的类字段timer
  2. 在办法m开端时增加代码timer -= System.currentTimeMillis();
  3. 在办法结束时增加timer += System.currentTimeMillis();System.out.println("test m used " + timer + "ms");

因而咱们需求自定义ClassVisitor和MethodVisitor:

public class TimerVisitor extends ClassVisitor {
    protected TimerVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM9, classVisitor);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if ("m".equals(name) && "()V".equals(descriptor)) {
            return new TimerMethodTransform(mv);
        }
        return mv;
    }
    @Override
    public void visitEnd() {
        super.visitEnd();
        FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "timer", "J", null, null);
        if (fv != null) {
            fv.visitEnd();
        }
    }
}

TimerVisitor在visitEnd时经过visitField新增timer字段,在visitMethod中,将办法m的MethodVisitor替换为TimerMethodTransform:

m()办法转化后的字节码如下(在idea系ide中可凭借ASM ByteCode Viewer插件辅佐检查):

GETSTATIC com/example/test/CTimer.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSUB # --1
PUTSTATIC com/example/test/CTimer.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep (J)V
GETSTATIC com/example/test/CTimer.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LADD  # --2
PUTSTATIC com/example/test/CTimer.timer : J
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "test m used "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
GETSTATIC com/example/test/CTimer.timer : J
INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
LDC "ms"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
LOCALVARIABLE this Lcom/example/test/CTimer;
MAXSTACK = 4
MAXLOCALS = 1

因为修正后的代码没有新增局部变量,因而局部变量表MAXLOCALS仍为1;细心剖析上面的字节码指令,会发现用到最多操作数栈的指令是 --1 --2处,其中

GETSTATIC com/example/test/CTimer.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis ()J

连续把两个long类型的数值压入操作数栈,因而其最大的操作数栈为MAXSTACK4。

依据如上字节码指令,咱们能够得到如下代码,重写visitCode办法表明在办法前刺进字节码指令。办法通常由一系列的RETURN指令或ATHROW指令介绍,因而在visitInsn中判断当时的操作码,如果是RETURN指令或ATHROW指令,则刺进代码:

public class TimerMethodTransform extends MethodVisitor {
    protected TimerMethodTransform(MethodVisitor methodVisitor) {
        super(Opcodes.ASM9, methodVisitor);
    }
    @Override
    public void visitCode() {
        super.visitCode();
        if (mv != null) {
            mv.visitFieldInsn(Opcodes.GETSTATIC, "com/example/test/C", "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LSUB);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/test/C", "timer", "J");
        }
    }
    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) && mv != null) {
            mv.visitFieldInsn(Opcodes.GETSTATIC, "com/example/test/C", "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitInsn(Opcodes.LADD);
            mv.visitFieldInsn(Opcodes.PUTSTATIC, "com/example/test/C", "timer", "J");
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("test m used ");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitFieldInsn(Opcodes.GETSTATIC, "com/example/test/C", "timer", "J");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitInsn(opcode);
    }
    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(4, maxLocals);
    }
}

结语

这篇文章总结了与Method相关的字节码虚拟机知识,并经过几个简略的比如对MethodVisitor相关api进行熟悉,计算办法履行时间的比如是一种无状况的办法转化,更为杂乱的办法转化规划字节码指令的状况,在做这类转化时需求细心规划状况机,因为暂时没有相关的转化需求,这儿就不进行实例讲解了。

参阅

asm4-guide