办法结构
履行模型
每个线程都有自己的虚拟机栈,用于存储栈帧:每逢一个办法被调用,会创立新的栈帧并压入虚拟机栈,办法履行结束后,栈帧从仓库中弹出。
每个栈帧都包括局部变量表和操作数栈:
局部变量表存储对象的this引用、办法参数以及办法内声明的变量,如果是实例办法,第0个局部变量保存this,这以后跟着办法参数。
long和double类型的数据占用两个连续的局部变量,运用较小的索引值定位。
操作数栈是一个后进先出的栈结构,其最大深度编译时确定,栈帧刚初始化时,操作数栈为空。
下面是一个虚拟机栈的示意图:
字节码指令
字节码指令由一个指令码和若干个参数组成,如下:
opcode arguments
首要分为两类:
- 将局部变量的数据加载到操作数栈
- 弹出操作数栈上的数据,履行核算后将成果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办法履行过程栈帧变化内容如下:
- 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 label
和GOTO 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
因而能够用visitCode
和visitMaxs
检测字节码中办法的开端方位,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");
}
}
咱们需求做以下三件事情来主动生成上面的代码:
- 增加long类型的类字段timer
- 在办法m开端时增加代码
timer -= System.currentTimeMillis();
- 在办法结束时增加
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