首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >遇到VerifyError束手无策?

遇到VerifyError束手无策?

作者头像
吴就业
发布2020-07-10 11:25:12
9050
发布2020-07-10 11:25:12
举报
文章被收录于专栏:Java艺术Java艺术

VerifyError通常是修改字节码引起的类加载阶段的验证错误。类加载过程分三个阶段,分别是加载、链接和初始化,而链接阶段又可细分为验证、准备和解析三个阶段。VerifyError异常发生在链接阶段的验证阶段。在学习使用asm动态生成字节码的过程中,我们或多或少都会遇到这个错误,那么遇到这个问题我们该如何解决呢?本篇文章教大家如何解决这个老大难的问题。对asm改写字节码不了解的读者也可以看一下,了解类的加载过程。

类的验证阶段在hotspot虚拟机中,是在类初始化之前执行的,我们使用ClassLoaderloadClass方法加载类时,如果加载完成后不使用,虚拟机是不会对这个类进行验证和初始化的。触发类初始化的字节码指令有newgetstaticsetstaticinvokestatic这四条指令,分别对应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命令来执行测试例子,程序输出的结果如下图所示。

从测试结果中可以看出,在ClassLoaderlocaClass方法执行完成后,我们就已经能够获取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你还会束手无策吗?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档