VerifyError
通常是修改字节码引起的类加载阶段的验证错误。类加载过程分三个阶段,分别是加载、链接和初始化,而链接阶段又可细分为验证、准备和解析三个阶段。 VerifyError
异常发生在链接阶段的验证阶段。在学习使用 asm
动态生成字节码的过程中,我们或多或少都会遇到这个错误,那么遇到这个问题我们该如何解决呢?本篇文章教大家如何解决这个老大难的问题。对 asm
改写字节码不了解的读者也可以看一下,了解类的加载过程。
类的验证阶段在 hotspot
虚拟机中,是在类初始化之前执行的,我们使用 ClassLoader
的 loadClass
方法加载类时,如果加载完成后不使用,虚拟机是不会对这个类进行验证和初始化的。触发类初始化的字节码指令有 new
、 getstatic
、 setstatic
、 invokestatic
这四条指令,分别对应 new
一个对象、访问该类的某个静态字段,调用该类的某个静态方法。
为验证类的字节码验证是发生在类初始化之前的,我修改了 hotspot
虚拟机源码,在一些链接、验证相关步骤的方法中加入了日记打印。测试类加载的代码程序如下。
public static void main(String[] args) throws Exception { Class<?> clz = LinkAndVerifyTest.class.getClassLoader() .loadClass("com.wujiuye.asmbytecode.book.fourth.VerifyTest2"); System.out.println(clz); try { Object target = clz.newInstance(); Method method = clz.getMethod("getId"); System.out.println("return value:" + method.invoke(target)); } catch (Exception e) { e.printStackTrace(); } } 复制代码
将修改后的 hotspot
源码重新编译后,我们再使用编译后的 java
命令来执行测试例子,程序输出的结果如下图所示。
从测试结果中可以看出,在 ClassLoader
的 locaClass
方法执行完成后,我们就已经能够获取 Class
对象,并且打印 Class
对象的类名,此时虚拟机的方法区中已经存在一个 InstanceKlass
实例。在通过反射创建对象时,才看到链接方法以及字节码验证方法中打印的日记,说明链接阶段并不是在加载阶段完成后立即执行的。
并且我将测试例子中的实例化并通过反射调用对象的方法这部分去掉后,就不会打印链接与验证字节码的相关日记,说明链接阶段确实是在初始化阶段触发的,在类初始化之前再去链接,包括完成字节码的验证工作。
很多人在遇到 VerifyError
时,从网上找到的答案都是加 -noverify
参数,虽然加 -noverify
参数可以忽略 VerifyError
异常,让程序正常跑起来,但去掉验证后,程序运行的过程中可能会出现问题。并且 -noverify
并不是忽略所有的验证错误,有些错误是忽略不了的。本篇将以一个例子教大家如何解决 VerifyError
。
为模拟类加载阶段抛出一个 VerifyError
,我使用 asm
编写了一个测试类,在实现这个测试类的实例初始化方法 <init>
时,我并未生成调用父类的实例初始化方法 <init>
。 asm
编写测试类的代码如下。
public static class VerifyTestByteCodeHandler implements ByteCodeHandler { private ClassWriter classWriter; public VerifyTestByteCodeHandler() { this.classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); } @Override public String getClassName() { return "com/wujiuye/asmbytecode/book/fourth/VerifyTestNew"; } private void voidConstructor() { // 生成<init>方法 MethodVisitor methodVisitor = this.classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); methodVisitor.visitCode(); // 调用父类构造器 // methodVisitor.visitVarInsn(ALOAD, 0); // methodVisitor.visitMethodInsn(INVOKESPECIAL, Object.class.getName().replace(".", "/"), // "<init>", "()V", false); methodVisitor.visitInsn(RETURN); methodVisitor.visitMaxs(1, 1); methodVisitor.visitEnd(); } @Override public byte[] getByteCode() { this.classWriter.visit(Opcodes.V1_8, ACC_PUBLIC, getClassName(), null, Object.class.getName().replace(".", "/"), null); voidConstructor(); this.classWriter.visitEnd(); return this.classWriter.toByteArray(); } } 复制代码
来看下 asm
编写的测试类输出的 class
文件使用 idea
反编译后的 java
代码。
public class VerifyTest2 { public VerifyTest2() { } } 复制代码
从反编译的 java
代码中,并看不出这个类有什么问题。现在我们编写测试代码,试着使用类加载器加载这个 class
。测试代码中用到的类加载器是自定义的类加载器。
public static void main(String[] args) throws Exception { ByteCodeClassLoader loader = new ByteCodeClassLoader(ClassLoader.getSystemClassLoader()); String cName = "com/wujiuye/asmbytecode/book/fourth/VerifyTestNew"; loader.add(cName, new VerifyTestByteCodeHandler()); Class<?> clz = loader.loadClass(cName); System.out.println(clz); } 复制代码
此测试代码是可以正常执行的,如下图。
但如果将测试代码改一下,通过反射创建一个对象。修改后的代码如下。
public static void main(String[] args) throws Exception { ByteCodeClassLoader loader = new ByteCodeClassLoader(ClassLoader.getSystemClassLoader()); String cName = "com/wujiuye/asmbytecode/book/fourth/VerifyTestNew"; loader.add(cName, new VerifyTestByteCodeHandler()); Class<?> clz = loader.loadClass(cName); System.out.println(clz); try { Object target = clz.newInstance(); } catch (Exception e) { e.printStackTrace(); } } 复制代码
此时就会抛出一个异常, java.lang.VerifyError: Constructor must call super() or this() before return
。两次测试结果不一样的原因是,字节码的验证是在类初始化之前才开始的,所以前面的测试代码没有问题,而反射创建对象会触发类的初始化,在类的初始化之前会判断这个类有没有链接,如果未链接则会完成链接。
程序输出的 VerifyError
是说明该类的实例初始化方法 <init>
中没有调用父类的实例初始化方法,这个例子很简单。但我们把它当成一个复杂的问题来看待,面对这个异常,我们如何解决。
从 hotspot
源码中找到抛出该异常的位置,字节码验证工作都是在 vm/classfile/verifier.cpp
这个 c++
代码文件中完成的。如例子中抛出的异常。
图为 hotspot
虚拟机 ClassVerifier
类的 verify_class
方法部分截图。这与测试例子抛出的异常描述相符,从源码中可以看到抛出异常的原因,在验证方法的最后一条 return
字节码指令时,如果当前方法名称是 <init>
,且并未找到调用父类的 <init>
方法的字节码指令,则抛出异常。
例子比较简单,所以看到这里也就知道怎么解决了,现在我们换一个比较难的例子。
这个例子抛出的 java.lang.VerifyError
描述信息是 Expecting a stackmap frame at branch target 27
,从虚拟机中找到的源码如下。
在验证栈映射桢的方法中抛出的,那栈映射桢是什么呢?我们可以从《 java
虚拟机规范》中有关属性的规定能够找到一个 StackMapTable
属性,这个属性用在虚拟机的类型检查验证阶段。《 java
虚拟机规范》中关于 StackMapTable
属性的描述如图所示。
因此,我们可以知道,这个异常的原因是由于我们编写的字节码中,需要通过 StackMapTable
属性使基本数据类型装箱。比如,调用一个方法描述符为 (Ljava/lang/Long)V
的方法,而传递的参数类型却是基本数据类型 J
(也就是 long
)。
我们也可以通过使用 java
代码写一个相同的类,然后使用 classpy
等字节码查看工具查看编译器生成的 class
文件的字节码,与通过 ASM
编写字节码生成的 class
文件的字节码对比,看两者的差异,从而找到问题的原因。
要从入门到进阶 java
虚拟机字节码,我们需要掌握的知识点不仅仅只是了解字节码指令以及怎么使用 asm
工具编写字节码,我们更需要对整个 class
文件结构有着非常熟悉的了解,以及对类加载、验证过程熟悉,而熟悉类加载过程最好的学习方法就是看 jvm
源码。
通过本篇的学习,遇到 VerifyError
你还会束手无策吗?