原创 吴就业 125 0 2020-04-12
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/dace982427f3418594553bbf4f73fbb7
作者:吴就业
链接:https://wujiuye.com/article/dace982427f3418594553bbf4f73fbb7
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
本篇文章写于2020年04月12日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。
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
你还会束手无策吗?
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
`Redis`多数据库是我在`Redis`设计中最糟糕的决定,我希望在某种程度上,我们可以放弃多个数据库的支持,但我认为可能已经太晚了,因为有很多人在工作中使用这个特性。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。