invokedynamic指令

Java虚拟机的字节码指令集的数量从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一直没有发生任何变化。随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言”(Dynamically Typed Language)支持而进行的改进之一,也是为JDK 8可以顺利实现Lambda表达式做技术准备。

动态类型语言

动态类型语言是什么?它与Java语言、Java虚拟机有什么关系?

什么是动态类型语言?注意,动态类型语言与动态语言、弱类型语言并不是一个概念,需要区别对待。

动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等。相对的,在编译期就进行类型检查过程的语言(如C++和Java等)就是最常用的静态类型语言。

觉得上面定义过于概念化?那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。

首先看下面这段简单的Java代码,它是否能正常编译和运行?---在编译期/运行期进行

public static void main(String[] args){
  int[][][] array=new int[1][0][-1];
}

这段代码能够正常编译,但运行的时候会报NegativeArraySizeException异常。在Java虚拟机规范中明确规定了NegativeArraySizeException是一个运行时异常,通俗一点来说,运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常,即使会导致连接时异常的代码放在一条无法执行到的分支路径上,类加载时(Java的连接过程不在编译阶段,而在类加载阶段)也照样会抛出异常。

例如下面这一句非常简单的代码:----类型检查

obj.println("hello world");

虽然每个人都能看懂这行代码要做什么,但对于计算机来说,这一行代码“没头没尾”是无法执行的,它需要一个具体的上下文才有讨论的意义。

现在假设这行代码是在Java语言中,并且变量obj的静态类型为java.io.PrintStream,那变量obj的实际类型就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj属于一个确实有用println(String)方法,但与PrintStream接口没有继承关系,代码依然不可能运行——因为类型检查不合法。

但是相同的代码在ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,只要这种类型的定义中确实包含有println(String)方法,那方法调用便可成功。

这种差别产生的原因是Java语言在编译期间已将println(String)方法完整的符号引用(本例中为一个CONSTANT_InterfaceMethodref_info常量)生成出来,作为方法调用指令的参数存储到Class文件中,例如下面这段代码:

invokevirtual#4;//Method java/io/PrintStream.println:(Ljava/lang/String;)V

这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机可以翻译出这个方法的直接引用。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有类型,编译时最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(即方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。

JDK 1.7与动态类型

Java虚拟机毫无疑问是Java语言的运行平台,但它的使命并不仅限于此,早在1997年出版的《Java虚拟机规范》中就规划了这样一个愿景:“在未来,我们会对Java虚拟机进行适当的扩展,以便更好地支持其他语言运行于Java虚拟机之上”。而目前确实已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等,能够在同一个虚拟机上可以达到静态类型语言的严谨性与动态类型语言的灵活性,这是一件很美妙的事情。

但遗憾的是,Java虚拟机层面对动态类型语言的支持一直都有所欠缺,主要表现在方法调用方面:JDK 1.7以前的字节码指令集中,4条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一个参数都是被调用的方法的符号引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型。这样,在Java虚拟机上实现的动态类型语言就不得不使用其他方式(如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配)来实现,这样势必让动态类型语言实现的复杂度增加,也可能带来额外的性能或者内存开销。尽管可以利用一些办法(如Call Site Caching)让这些开销尽量变小,但这种底层问题终归是应当在虚拟机层次上去解决才最合适,因此在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一,这就是JDK 1.7(JSR-292)中invokedynamic指令以及java.lang.invoke包出现的技术背景。

java.lang.invoke包

JDK 1.7实现了JSR-292,新加入的java.lang.invoke包。这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle。

MethodHandle的基本用途,无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确地调用到println()方法。

public class MethodHandleTest{
    static class ClassA{
        public void println(String s){
           System.out.println(s);
        }
    }
    public static void main(String[]args)throws Throwable{
        Object obj=System.currentTimeMillis()%2==0?System.out:new ClassA();
        /*无论obj最终是哪个实现类,下面这句都能正确调用到println方法*/
        getPrintlnMH(obj).invokeExact("icyfenix");
    }
    private static MethodHandle getPrintlnMH(Object reveiver)throws Throwable{
      /*MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)*/
        MethodType mt=MethodType.methodType(void.class,String.class);
        /*lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄*/
        /*因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象,      这个参数以前是放在参数列表中进行传递的,而现在提供了bindTo()方法来完成这件事情*/
        return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver);
    }
}

实际上,方法getPrintlnMH()中模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上,而是通过一个具体方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。

相同的事情,用反射不是早就可以实现了吗?

仅站在Java语言的角度来看,MethodHandle的使用方法和效果与Reflection有众多相似之处,不过,它们还是有以下这些区别:

  1. 从本质上讲,Reflection和MethodHandle机制都是在模拟方法调用,但Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.lookup中的3个方法——findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual&invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
  2. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含执行权限等的运行期信息。而后者仅仅包含与执行该方法相关的信息。用通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。
  3. 由于MethodHandle是对字节码的方法指令调用的模拟,所以理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
  4. MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度来看”:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计成可服务于所有Java虚拟机之上的语言,其中也包括Java语言。

invokedynamic指令

在某种程度上,invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有4条“invoke*”指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以把它们想象成为了达成同一个目的,一个采用上层Java代码和API来实现,另一个用字节码和Class中其他属性、常量来完成。因此,如果理解了前面的MethodHandle例子,那么理解invokedynamic指令也并不困难。

每一处含有invokedynamic指令的位置都称做“动态调用点”(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

举一个实际的例子来解释这个过程,如下所示。

import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class InvokeDynamicTest{
    public static void main(String[]args)throws Throwable{
        INDY_BootstrapMethod().invokeExact("icyfenix");
    }
    public static void testMethod(String s){
        System.out.println("hello String:"+s);
    }
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup,String name,MethodType mt)throws Throwable{
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class,name,mt));
    }
    private static MethodType MT_BootstrapMethod(){
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles $Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",null);
    }
    private static MethodHandle MH_BootstrapMethod()throws Throwable{
        return lookup().findStatic(InvokeDynamicTest.class,"BootstrapMethod",MT_BootstrapMethod());
    }
    private static MethodHandle INDY_BootstrapMethod()throws Throwable{
        CallSite cs=(CallSite)MH_BootstrapMethod().invokeWithArguments(lookup(),"testMethod",MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V",null));
        return cs.dynamicInvoker();
    }
}

看一下BootstrapMethod(),所有逻辑就是调用MethodHandles $Lookup的findStatic()方法,产生testMethod()方法的MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。

掌控方法分派规则

invokedynamic指令与前面4条“invoke*”指令的最大差别就是它的分派逻辑不是由虚拟机决定的,而是由程序员决定。

class GrandFather{
    void thinking(){
        System.out.println("i am grandfather");
    }
}

class Father extends GrandFather{
    void thinking(){
        System.out.println("i am father");
    }
}
class Son extends Father{
    void thinking(){
        //请在这里填入适当的代码(不能修改其他地方的代码)
        //实现调用祖父类的thinking()方法,打印"i am grandfather"
    }
}

在Java程序中,可以通过“super”关键字很方便地调用到父类中的方法,但如果要访问祖类的方法呢?

在JDK 1.7之前,使用纯粹的Java语言很难处理这个问题(直接生成字节码就很简单,如使用ASM等字节码工具),原因是在Son类的thinking()方法中无法获取一个实际类型是GrandFather的对象引用,而invokevirtual指令的分派逻辑就是按照方法接收者的实际类型进行分派,这个逻辑是固化在虚拟机中的,程序员无法改变。在JDK 1.7中,可以使用MethodHandle来解决相关问题。

时间: 2024-08-09 10:50:43

invokedynamic指令的相关文章

Java8 Lambda表达式深入学习(2) -- InvokeDynamic指令详解

为了更好的支持动态类型语言,Java7通过JSR292给JVM增加了一条新的字节码指令:invokedynamic.之后,JVM上面的一些动态类型语言,比如Groovy(2.0+)和JRuby(1.7.0+)都开始支持invokedynamic.不过让人意外的是,为动态语言量身定制的invokedynamic指令,居然也被用到了Java8的Lambda表达式(JSR335)实现上.本文会对invokedynamic(以下简写做indy)指令做出详细解释. 测试代码 Java7以及更早版本的Jav

Scripting Java #3:Groovy与invokedynamic

今天来简单看下Groovy语言的实现机制.在那之前得先来扯下静态类型与动态类型语言在实现上面的一些差异. 静态类型 vs. 动态类型 看下面这个简单的栗子, def addtwo(a, b) { return a + b; } 静态类型语言与动态类型语言对于上面这个简单的加法实现完全不同.静态类型语言,例如Java,语言的编译器在编译时就已经进行类型检查,所以能够将+运算符编译成特定的指令,语言的runtime系统可以直接执行该指令.例如javac会将两个int类型的+运算编译成iadd指令,运

java7 invokedynamic命令研究

在看java虚拟机字节码执行引擎的时候,里面提到了java虚拟机里调用方法的字节码指令有5种: invokestatic //调用静态方法 invokespecial //调用私有方法.实例构造器方法.父类方法 invokevirtual //调用实例方法 invokeinterface //调用接口方法,会在运行时再确定一个实现此接口的对象 invokedynamic //先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部

java动态语言invokedynamic(2)

从某种程度上,invokedynamic与MethodHandle机制的作用是一样的,都是为了解决原来的4条指令"invoke*"指令方法将分派规则固化在虚拟机中的问题,如何将查找方法的决定权从虚拟机转移到具体的用户代码中.可将它们想象成一个使用上层的java API实现,另一个使用字节码中和class中的其它属性,常量来完成. 含有invokedynamic指令的位置被称为动态调用点(Dynamic Call Site),这个指令的第一个参数不再是代表方法符号引用 的CONSTANT

Invokedynamic——Java的秘密武器

在Java 7的发布版中包含了多项新的特性,这些特性乍看上去Java开发人员对它们的使用非常有限,在我们之前的文章中,曾经对其进行过介绍. 但是,其中有项特性对于实现Java 8中"头版标题"类型的特性来说至关重要(如lambdas和默认方法).在本文中,我们将会深入学习invokedynamic,并阐述它对于Java平台以及像JRuby和Nashorn这样的JVM语言来讲为何如此重要. invokedynamic最初的工作 至少始于2007年,而第一次成功的动态调用发生在2008年8

Java字节码指令

1. 简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成. 由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码. Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条. 2. 字节码和数据类型 在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息.例如,iload指令用于从局部变量中加

Jvm(48),指令集----方法调用和指令返回

方法调用(分派.执行过程)将在第8章具体讲解,这里仅列举以下5条用于方法调用的指令. invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式. invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法.私有方法和父类方法. invokestatic指令用于调用类方法

Scala Structural Typing结构类型

鸭子类型:"当看到一只鸟走起来像鸭子.游泳起来像鸭子.叫起来也像鸭子,那么这只鸟就可以被称为鸭子." 一般在动态语言里面才有,因为静态语言是强类型的,会在编译期检查类型,很难实现.但是Scala作为一门static type语言,居然支持,不得不说Scala确实很强大.直接上code package testscala object StructuralTyping extends App { def quacker(duck: {def quack(value: String): S

程序员必读书单

作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文地址:http://www.cnblogs.com/figure9/p/developer-reading-list.html 关于 本文把程序员所需掌握的关键知识总结为三大类19个关键概念,然后给出了掌握每个关键概念所需的入门书籍,必读书籍,以及延伸阅读.旨在成为最好最全面的程序员必读书单. 前言 Reading makes a full man; conference a ready man; and writing