VerifyError
一般是修改字节码引发的类加载阶段的验证错误。类加载过程分三个阶段,分别是加载、连接和初始化,而连接阶段又可细分为验证、准备和解析三个阶段。VerifyError
异常发生在连接阶段的验证阶段。在学习使用asm
动态生成字节码的过程当中,咱们或多或少都会遇到这个错误,那么遇到这个问题咱们该如何解决呢?本篇文章教你们如何解决这个老大难的问题。对asm
改写字节码不了解的读者也能够看一下,了解类的加载过程。java
类的验证阶段在hotspot
虚拟机中,是在类初始化以前执行的,咱们使用ClassLoader
的loadClass
方法加载类时,若是加载完成后不使用,虚拟机是不会对这个类进行验证和初始化的。触发类初始化的字节码指令有new
、getstatic
、setstatic
、invokestatic
这四条指令,分别对应new
一个对象、访问该类的某个静态字段,调用该类的某个静态方法。c++
为验证类的字节码验证是发生在类初始化以前的,我修改了hotspot
虚拟机源码,在一些连接、验证相关步骤的方法中加入了日记打印。测试类加载的代码程序以下。bash
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
命令来执行测试例子,程序输出的结果以下图所示。jvm
从测试结果中能够看出,在ClassLoader
的locaClass
方法执行完成后,咱们就已经可以获取Class
对象,而且打印Class
对象的类名,此时虚拟机的方法区中已经存在一个InstanceKlass
实例。在经过反射建立对象时,才看到连接方法以及字节码验证方法中打印的日记,说明连接阶段并非在加载阶段完成后当即执行的。ide
而且我将测试例子中的实例化并经过反射调用对象的方法这部分去掉后,就不会打印连接与验证字节码的相关日记,说明连接阶段确实是在初始化阶段触发的,在类初始化以前再去连接,包括完成字节码的验证工做。工具
不少人在遇到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
代码。ui
public class VerifyTest2 {
public VerifyTest2() {
}
}
复制代码
从反编译的java
代码中,并看不出这个类有什么问题。如今咱们编写测试代码,试着使用类加载器加载这个class
。测试代码中用到的类加载器是自定义的类加载器。this
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
你还会一筹莫展吗?