java7 invokedynamic命令深入研究

在看java虛擬機字節碼執行引擎的時候,裏面提到了java虛擬機裏調用方法的字節碼指令有5種:

    1. invokestatic  //調用靜態方法
    2. invokespecial  //調用私有方法、實例構造器方法、父類方法
    3. invokevirtual  //調用實例方法
    4. invokeinterface  //調用接口方法,會在運行時再確定一個實現此接口的對象
    5. invokedynamic  //先在運行時動態解析出調用點限定符所引用的方法,然後再執行該方法,在此之前的4條調用指令,分派邏輯是固化在java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

前4種很容易理解,但是第5種筆者本人從這段描述上無法理解這個invokedynamic到底是什麼東西,於是決定從實踐入手來剖析一下。

invokedynamic本身是字節碼命令,我們想直接調用這個命令只能手寫java字節碼,這個難度太大了。。有沒有替換方案呢,答案是有的。

ASM簡介

官方的定義:ASM是一個java字節碼操作和分析框架。可以用來編輯classes文件和直接動態生成class文件,一切都是直接基於二進制形式的。

我來解釋下:我們都知道,一個.java文件編譯後會生成.class文件,.java文件中記錄代碼的形式是java源代碼,而.class文件中記錄代碼的形式是java字節碼,這兩者本質上是以不同的形式存儲相同的內容,兩者也可以相關轉換(編譯和反編譯)。而asm本身是一個java庫,所以說編寫asm代碼的時候本質是在寫java源代碼,但是asm代碼的最終目的並不是爲了運行,而是爲了生成字節碼。

舉個例子:

我現在有一個Test.java類:

複製代碼
package common;

public class Test {
    public void say(){
        System.out.println("Hi");
    }
}
 
複製代碼

這個類編譯後生成Test.class,Test.class文件裏存儲的實際上就是一個byte數組,但是我們可以用javap -verbose命令翻譯爲字節碼命令查看(忽略常量池等無關信息,只截取say()方法的CODE碼):

複製代碼
 public void say();
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #21                 // String Hi
        5: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
複製代碼

我標紅的部分就是從字節碼翻譯過來的字節碼命令,字節碼和字節碼命令是一一對應的,如invokevirtual命令在源文件中就是一個字節 0xb6,對應關係可以查表:http://www.cnblogs.com/sheeva/p/6279096.html

看到這裏我們會發現,這裏的invokevirtual命令就是java調用方法的5種字節碼中的第3種,如果我們能夠修改這裏的invokevirtual改成invokedynamic我們就能搞清楚invokedynamic到底是做什麼的了,但是javap命令只能以命令的形式查看字節碼卻不能修改,這時候就輪到asm登場了。

下載 asm5.2:http://download.forge.ow2.org/asm/asm-5.2-bin.zip

解壓後在lib裏找到asm-all-5.2.jar放到Test.class的目錄下,執行:

java -classpath "./*" org.objectweb.asm.util.ASMifier Test.class,

運行結果如圖:

生成了一個java類的源碼,裏面有一個dump()方法,這個方法返回值是byte[],這個byte[]的內容就是Test.class的字節碼,也就是說如果把方法的返回值保存到一個文件,那麼這個文件和Test.class文件是完全一樣的。

爲了驗證這個結論,我把這個方法粘貼出來,自己寫一個類加載器來加載方法返回的字節碼然後調用say()方法:

複製代碼
package invokedynamic;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class Hello implements Opcodes {
    
    public static void main(String[] args) throws Exception {
        byte[] codes=dump();
        Class<?> clazz=new MyClassLoader().defineClass("common.Test", codes);
        clazz.getMethod("say", null).invoke(clazz.newInstance(), new Object[]{});
    }

    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "common/Test", null, "java/lang/Object", null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
            mv.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hi");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
    
    
    private static class MyClassLoader extends ClassLoader implements Opcodes {
        public Class<?> defineClass(String name, byte[] b){
            return super.defineClass(name, b, 0, b.length);
        }

    }
}
複製代碼

運行成功:

現在我們確定了,這個dump()方法確實能夠生成Test.class的字節碼,現在來看一下dump()方法裏的內容:

複製代碼
    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "common/Test", null, "java/lang/Object", null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv.visitInsn(RETURN);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
            mv.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hi");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
複製代碼

如果熟悉字節碼的話,應該已經看出來了,dump()這個方法所在的類實現了Opcodes接口,Opcodes接口裏定義了幾乎全部的java字節碼命令,我們之前說的5個invoke命令也在內:

然後看一下dump()方法裏我標紅的4句,和之前的javap命令打出來的Test.class字節碼命令對照看:

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hi");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);

 

複製代碼
 public void say();
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #21                 // String Hi
        5: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
複製代碼

不需要解釋了吧。。

用asm調用invokedynamic指令

現在我們來把原來say方法裏的通過invokevirtual輸出Hi的代碼去掉,改成通過invokedynamic輸出hello。

在dump()方法所在的類Hello類的包里加一個類Bootstrap:

複製代碼
package invokedynamic;

import java.lang.invoke.*;

public class Bootstrap {

    private static void hello() {
        System.out.println("Hello!");
    }

    public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        Class thisClass = lookup.lookupClass();
        MethodHandle mh = lookup.findStatic(thisClass, "hello", MethodType.methodType(void.class));
        return new ConstantCallSite(mh.asType(type));
    }
}
複製代碼

Hello類把say()方法裏原來通過invokevirtual調用System.out.println()方法的那幾行去掉,換成動態調用:

複製代碼
        {
            mv = cw.visitMethod(ACC_PUBLIC, "say", "()V", null, null);
            mv.visitCode();
            
//            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//            mv.visitLdcInsn("Hi");
//            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
//            mv.visitInsn(RETURN);
//            mv.visitMaxs(2, 1);
            
            MethodType mt = MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class,
                    MethodType.class);
            Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, "invokedynamic/Bootstrap", "bootstrap",
                    mt.toMethodDescriptorString());
            mv.visitInvokeDynamicInsn("dynamicInvoke", "()V", bootstrap);
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 1);
            
            
            mv.visitEnd();
        }
複製代碼

再次運行,這次輸出變了:

結論

現在我們結合我們得到的代碼,再重新理解一下invokedynamic的定義:

先在運行時動態解析出調用點限定符所引用的方法,  //即通過bootstrap方法動態解析出hello方法

然後再執行該方法,  //即執行hello方法

而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。  //這裏的引導方法,即我們定義的bootstrap方法,這裏我們的邏輯是直接分派了hello方法,但是我們也可以寫一些邏輯,比如根據調用時候的參數類型來動態決定調用哪個方法

 

現在我們已經自己實踐了invokedynamic命令的使用,但是我相信很多人還是不明白這個命令的意義所在,這要從語言的靜態類型和動態類型說起:

靜態類型就是每個變量在初始化的時候就要聲明唯一的類型並且不能改變。

動態類型就是說變量沒有固定類型,變量的類型取決於它裏面元素的類型。

java語言是靜態類型的。有人可能會提到泛型,java的泛型是擦除式的,也就是說雖然在編寫java源碼時看起來好像不能確定變量類型,但是在java編譯爲字節碼的過程中,每一個變量都是有確定的類型的。

所以從java語言的角度,之前的4條方法調用指令是完全夠用的,但是要知道,jvm不只是跨平臺的,還是跨語言的,當有人在jvm上試圖開發動態類型語言的時候,問題就來了:

jvm大多數指令都是類型無關的,但是在方法調用的時候,卻不是這樣,每個方法調用在編譯階段就必須指明方法參數和返回值類型,但是動態類型語言的方法參數,直到運行時刻才能知道類型啊,因此jdk就做了這樣一個“補丁”:用invokedynamic調用方法的時候,會轉到bootstrap方法,在這個方法裏可以動態獲取參數類型,然後根據參數類型分派合適的方法作爲CallSite(動態調用點),最後真實調用的就是CallSize裏的方法。如此便能在jvm上實現動態類型語言的方法調用了。


原文地址:https://www.cnblogs.com/sheeva/p/6344388.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章