使用asm工具为一个函数添加局部变量后,重新加载该类失败。本篇文章主要分析错误的原因以及解决的办法。
首先了解一下虚拟机的类加载机制。类在初始化之前要完成加载、验证、准备和解析。
加载
加载过程分为三步:
1,通过一个类的全限定名获取定义这个类的二进制流
2,将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
3,在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
验证
由编译器编译通过的class文件是经过检查的,不存在跳转到不存在的代码行、转化为未实现的类型以及静态的数组访问越界等情况。但是由其他方式产生的class文件或者经过ASM和AOP工具改写过的class文件不一定是安全的。所以虚拟机为了避免载入危险的字节码而导致系统崩溃,必须对载入的class进行验证。该过程主要包括四种类型的验证:
1,文件格式验证
验证字节流是否符合class文件的格式规范,是否能被当前版本的虚拟机处理
2,元数据验证
对字节码描述的信息(即类的元数据信息)进行分析,以保证其描述的信息符合Java语言规范的要求(如:该类是否有父类、是否继承了不允许被继承的类等)
3,字节码验证
进行数据流和控制流分析,即对类的方法体进行校验分析以保证被校验类的方法在运行时不会做出危害虚拟机安全的行为(如:保证跳转指令不会跳转到方法体以外的字节码指令上等)
即使一个方法通过了字节码验证,也不能说明其一定是安全的(通过程序去校验程序逻辑是无法做到绝对准确的)
4,符号引用验证
对类自身以外的信息进行匹配性校验(如:符号引用中通过字符串描述的全限定名是否能找到对应的类、指定的类中是否存在符合描述符与简单名称描述的方法与字段)
准备
该阶段为类变量分配内存,并设置初始值,即该变量类型的零值。
在了解了类加载机制以后,我们来分析添加局部变量后,重新加载失败的原因。JDK1.6之后的Javac编译器进行了一项优化,给方法体的Code属性的属性表中增加了一项新属性:StackMapTable,该属性保存了方法体中的类型信息,可以使字节码验证时的类型推导变为类型检查从而节省时间。每项stack_map_frame显式或隐式得记录了字节码的偏移量、局部变量验证类型和操作栈的验证类型。验证器就是通过获取局部变量类型和操作栈类型进行验证的。而使用ASM工具在函数内部添加局部变量后,
并没有改变StackMapTable中的值,导致类在验证时不通过。具体错误参见下面的代码。
Exception in thread "main" java.lang.VerifyError: Inconsistent stackmap frames at branch target 26
Exception Details:
Location:
com/vlis/ostm/codeenhance/LocalClassTestExample.setInt1(I)V @26: return
Reason:
Type top (current frame, locals[2]) is not assignable to integer (stack map, locals[2])
Current Frame:
bci: @2
flags: { }
locals: { 'com/vlis/ostm/codeenhance/LocalClassTestExample', integer }
stack: { integer, integer }
Stackmap Frame:
bci: @26
flags: { }
locals: { 'com/vlis/ostm/codeenhance/LocalClassTestExample', integer, integer }
stack: { }
Bytecode:
0000000: 1b04 a000 182a 1b2a b400 943d b500 942a
0000010: 1295 2ab4 0094 1cb8 0098 b1
Stackmap Table:
append_frame(@26,Integer)
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2570)
at java.lang.Class.getMethod0(Class.java:2813)
at java.lang.Class.getMethod(Class.java:1663)
at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)
解决方法:
在JDK1.6的HotSpot虚拟机中提供了-XX:-UseSplitVerifier选项来关闭这项优化,或者使用-XX:+FailOverToOldVerifier选项在类型检查失败时退回到使用类型推导方式进行校验。添加该参数后使用asm添加了局部变量的类即可正确加载。