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上实现动态类型语言的方法调用了。

时间: 2024-11-05 02:41:04

java7 invokedynamic命令研究的相关文章

Linux下wc命令研究

wc word count  文本统计命令 是Linux用来统计数字的小命令 会输出3个数字,分别是 行  单词  字节 这里 行:  包括空行 单词:不包括空白符,不包括结束符$ 字节:包括空白符,包括结束符$ 1字节 其主要参数和特性如下 命令     关键字           解释及特性 -l  line 只显示行 -w word  只显示单词 -c bytes count 只显示字节数 (包括结束符1字节) -m  chars(任性) 只显示字符数(包括结束符1字符) -L line

powershell5.0中的ConvertFrom-String命令研究

-------先上个例子------- $aaa = @'0.027 0.034 0.834 0.1050.346 0.558 0.018 0.0780.001 0.997 0.001 0.0010.994 0.001 0.004 0.0010.001 0.996 0.002 0.0010.001 0.001 0.997 0.0010.001 0.009 0.001 0.9890.051 0.111 0.837 0.001'@ $t = @'{字段名1*:0.027} {字段名2:0.034}

oracle的expdp和impdp命令研究

--创建远程数据连接 create database link DB_MZDB11  connect to YTMZTWO identified by YTMZTWO909  using 'MZDB11'; --创建本地目录 create directory db_mz as 'c:'; --授权用户读写 grant read,write on directory db_mz to YTMZTWO,YTMZ; --授权用户可以创建dblink grant create database link

Java8新特性之lambda

本系列文章翻译自@shekhargulati的java8-the-missing-tutorial Java8中最重要的特性之一就是引入了lambda表达式.这能够使你的代码更加简练,并允许你将行为传递到各处.一段时间以来,Java因为自身的冗长和缺少函数式编程的能力而受到批评.随着函数式编程变得越来越流行和有价值,Java也在努力接受函数式编程.否则,Java将会变得没有价值. Java8在使世界上最受欢迎的编程语言之一在接纳函数式编程的过程中向前迈了一大步.为了支持函数式编程,编程语言必须将

Java新特性之Nashorn的实例详解

Nashorn是什么 Nashorn,发音"nass-horn",是德国二战时一个坦克的命名,同时也是java8新一代的javascript引擎--替代老旧,缓慢的Rhino,符合 ECMAScript-262 5.1 版语言规范.你可能想javascript是运行在web浏览器,提供对html各种dom操作,但是Nashorn不支持浏览器DOM的对象.这个需要注意的一个点. 关于Nashorn的入门 主要是两个方面,jjs工具以及javax.script包下面的API: jjs是在j

pssh 批量管理工具使用方法

管理上千服务器而且要并发执行要么字写工具用开源的也不错, 这类工具比如 pdsh,mussh,cssh,dsh等还有这里提到的pssh:1  安装: #wget http://peak.telecommunity.com/dist/ez_setup.pypython ez_setup.py#wget http://parallel-ssh.googlecode.com/files/pssh-2.2.2.tar.gz# tar zxvf pssh-2.2.2.tar.gz# cd pssh-2.2

Appcmd&amp;Adsutil.vbs基本用法及深入了解

微软官方介绍Appcmd.exe (IIS 7) :  https://technet.microsoft.com/zh-cn/library/cc772200(WS.10).aspx IIS7.0与IIS8.0 Appcmd 命令详解_Zoomla!逐浪CMS官网 https://www.zoomla.cn/help/tech/2276.shtml IIS7 全新管理工具AppCmd.exe的命令使用实例分享_win服务器_脚本之家 http://www.jb51.net/article/36

pssh,pdsh,mussh,cssh,dsh运维工具介绍

pssh 1 安装:#wget http://peak.telecommunity.com/dist/ez_setup.pypython ez_setup.py#wget http://parallel-ssh.googlecode.com/files/pssh-2.2.2.tar.gz# tar zxvf pssh-2.2.2.tar.gz# cd pssh-2.2.2# python setup.py install2 pssh使用 (假设ssh已做好SSH信任,ssh信任请参看:关于ssh

配置android source 在ubuntu中编译环境

在Ubuntu中可以配置 android source 编译环境,推荐使用最新的64位的Ubuntu  LTS(Long Time Support); 1.安装JDK. AOSP主分支代码需要java7,在Ubuntu上可以使用 OpenJDK. 安装java7的命令: $ sudo apt-get update $ sudo apt-get install openjdk-7-jdk 如果系统中已安装有java环境,可以将其进行更新: $ sudo update-alternatives --