遇到VerifyError一籌莫展?

VerifyError一般是修改字節碼引發的類加載階段的驗證錯誤。類加載過程分三個階段,分別是加載、連接和初始化,而連接階段又可細分爲驗證、準備和解析三個階段。VerifyError異常發生在連接階段的驗證階段。在學習使用asm動態生成字節碼的過程當中,咱們或多或少都會遇到這個錯誤,那麼遇到這個問題咱們該如何解決呢?本篇文章教你們如何解決這個老大難的問題。對asm改寫字節碼不瞭解的讀者也能夠看一下,瞭解類的加載過程。java

類的驗證階段在hotspot虛擬機中,是在類初始化以前執行的,咱們使用ClassLoaderloadClass方法加載類時,若是加載完成後不使用,虛擬機是不會對這個類進行驗證和初始化的。觸發類初始化的字節碼指令有newgetstaticsetstaticinvokestatic這四條指令,分別對應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

從測試結果中能夠看出,在ClassLoaderlocaClass方法執行完成後,咱們就已經可以獲取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你還會一籌莫展嗎?

相關文章
相關標籤/搜索