Java语言的动态性-invokedynamic

季延彬  |  2017. 06. 16   |  阅读 1754 次

概述

Invokedynamic指令在JAVA7中就已经提供了,在java7之前,JVM字节码提供了如下4种字节码方法调用指令:

1、 Invokevirtual:根据虚方法表调用虚方法。
2、 invokespecial,:调用实例构造方法(方法),私有方法,父类继承方法。
3、 invokeinteface:调用接口方法。
4、 invokestatic:调用静态方法
JVM字节码指令集一直比较稳定,一直到JAVA7中才增加了一个invokedynamic指令,这是JAVA为了实现『动态类型语言』支持而做的一种改进。但是在JAVA7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到JAVA8的Lambda表达式的出现,invokedynamic指令的生成,在java中才有了直接的生成方式。下面详细介绍java7之后对动态类型语言的支持。

动态类型语言和静态类型语言

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。说的在直白一点就是静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

Java7中增加的动态语言类型支持的本质是对java虚拟机规范的修改,而不是对JAVA语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在JAVA平台的动态语言的编译器。

动态类型语言的支持对应的JSR 292,主要包括两部份,一个是JAVA标准库中的新的方法调用API,另一个是JAVA虚拟机规范中新增加的invokedynamic指令。

普通调用指令

前面概述部份提到了java7之前的4种方法调用指令,下面结合实际例子来看一下字节码中的调用指令: public interface InvokeInterface {
void invokeInterface(); }

public class InvokeInterfaceImpl implements InvokeInterface{
public void invokeInterface(){};

public void invokeNormalMethod(){};

public static void invokeStaticMethod(){};

}

public class JavaCommonInvokeInstruction {

public void invoke(){
    //invokespeicial
    InvokeInterface sample = new InvokeInterfaceImpl();
    //invokeinterface
    sample.invokeInterface();
    InvokeInterfaceImpl sampleImpl = new InvokeInterfaceImpl();
    //invokevirtual
    sampleImpl.invokeNormalMethod();
    //invokestatic
    InvokeInterfaceImpl.invokeStaticMethod();
}

} 编译后,查看JavaCommonInvokeInstruction#invoke()字节码 javap –v JavaCommonInvokeInstruction 下图截取了invoke的字节调用码,不包括局部变量表等其它信息。也省略了类的其它信息,如常量池等。 图1 上图中,1-5的序号就是java中4种普通方法调用指令。其中1和3是同样的,都是对象的构造方法调用(私有方法调用,父类方法调用也是这个指令)。 2处是接口方法调用指令。4处是普通方法的调用,这个指令用于调用类中的一般方法。5处是类的静态方法调用。 除了invokestatic指令外,其它三个方法调用指令都有一个接收对像。或者说invokestatic的接收者是一个类。对于另外三种指令的接收者来说,都有一个在静太类型,也就是编译期确定的对象类型,对于invokespecial和invokevirtual来说静态类型就是接收者的对象类型。对于invokeinterface来说,静态类型就是接口的类型。

存在编译期确定的静态类型,那同样存在运行期才确认的动态类型。动态类型可能和静态类型相同,也可能不同;如果不同,则动态类型肯定是静态类型的子类型,否则字节码校验时会不通过。

方法分派

静态类型和动态类型又和java的方法分派有着密不可分的关系。方法的分派根据方法版本确定的时机分为静态分派,动态分派。

我们知道,java中所有方法的调用在class文件中都对应一个常量池中的符号引用,在类加载的解析阶段,符号引用会被解析为直接引用,有一类方法调用在运行之前就可确定一个唯一版本,这个版本在运行时不可变,我们称这类方法调用为解析,对应的方法调用字节码为:invokespecial和invokestatic。静态方法,构造方法,私有方法以及父类方法(通过super调用的方法)都无法通过继承的方式被覆盖,因为在编译期就能确定其版本。我们称这类分派为静态分派,方法的重载是这种类型的典型场景。

另一种分派为动态分派,故名思义,是在编译期无法确定其接收者真实类型,编译期只能确定方法接收者的静态类型,根据静态类型确定所调用方法的签名,无法知道其运行期值的类型,具体调用哪个类的相应方法版本只有在运行期根据接收者的实际类型来确定。方法的重写是这种应用的典型场景。对应的方法调用字节码为:invokevirtual和invokeinterface。

《深入java虚拟机》一收中提到,方法的分配根据宗量(影响最终方法确认的变量种数,如参数,接收者实际类型等)数的多少,分派又分为单分派和多分派。Java在编译期根据方法接收者的静态类型和方法实际参数类型确定方法的最终签名,所以java是一种静态多分派语言。同时,方法的签名一旦在编译期确定之后,运行期不再关心传入参数的类型,而只关心接收者的实际类型,所以运行期景响方法调用的宗量只有一个接收者实际值类型,所以java是一种动态单分派语言。

以上描述大家可以通过重载和覆盖来详细理解。另外,上述状态仅在java7之前版本存在。Java7之后增加了jvm对动态类型语言的支持,使java完全支持了动态多分派特性。Java7之后从虚拟机层面直接加入了对动态类型语言的支持,这就是大名鼎鼎的invokedynamic和java.lang.invoke包。

Java.lang.invoke包

JSR-292是JVM为动态类型支持而出现的规范,在JAVA7中实现了这个规则,这个包的主要作用就在之前只能依赖符号引用来确定目标方法的基础上,增加了一种动态确定目标方法的机制,也就是方法句柄MethodHandler。这有点类似于C++中的函数指针。从功能上讲,方法句柄类似于反射中的Method类,但两者之间有区别,方法句柄是轻量级的,我们从Method和MethodHandler的实现上可以看出来,Method的invoke方法会涉及到JAVA的安全访问检查,而方法句柄的所有invoke方法都是native方法,其性能优于反射(后面会给出性能对比示例);有一点相同,反射和方法句柄都是通过JAVA7之前的4种invoke方法调用指令实现的。

反射

如下例: public class ReflectionMain {
public void reflection() throws Exception { String methodName = "length"; Method method = String.class.getDeclaredMethod(methodName); Object result = method.invoke("abc"); }

} 上述java类模拟调用string类的length方法,其对应的字节码如下,未列出所有字节码内容,只列出来了方法的对应的字节码。 图2

方法句柄

下面给出一个方法句柄的基础使用示例,可以对比和反射的区别: public class InvokeExact { public void invokeExact() throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType type = MethodType.methodType(String.class, int.class, int.class); MethodHandle mh = lookup.findVirtual(String.class, "substring", type); String str = (String) mh.invokeExact("Hello World", 1, 3); } } 可以看出方法句柄的使用与方法类型要配合。Invoke包把方法类型使用MethodType描述,此对象主要表示方法的签名。MethodHandler和MethodType的组合可以更灵活多变。下面是上述类对应的字节码,本质上与反射没区别: 图3

方法类型

MethodType提供了多种工厂方法来获取实例,主要使用methodType方法,这个方法有多种形式的重载,它主要是通过返回值类型和参数类型来创建MethodType实例。其最少要有一个参数,表示方法返回值,参数可以有0个。methodType方法第一个参数表示返回值。
MethodType类提供了一个比较特殊的工厂方法:fromMethodDescriptorString,这个方法允许使用JAVA字节码中的类型描述字符串来创建MethodType,这种方法适合对字节码的类型描述比较熟悉的开发人员。如下例:
` public class MethodTypeFromMethodDescriptor {

public void generateMethodTypesFromDescriptor() {
    ClassLoader cl = this.getClass().getClassLoader();
    String descriptor = "(Ljava/lang/String;)Ljava/lang/String;";
    MethodType mt1 = MethodType.fromMethodDescriptorString(descriptor, cl);
    System.out.println(mt1);
}

` 获取MethodType实例后,可以对其进行修改,修改后得到一个新的MethodType实例,修改包括修改返回值,插入删除参数,修改已有参数等。还可以在基本类型和包装类型之间转换。不作详细描述。

方法句柄应用

方法调用

参考前面InvokeExact类的invokeExact方法,此方法使用了方法句柄的invokeExact方法去调用底层方法。此方法有比较严格的限制,其返回类型要和MethodType的返回值严格相同,也就是方法调用前的(String)强制类型转换不可省略,即使不赋值给一个变量,省略了强制类型,JVM会认为其返回值类型为void,这也和MethodType不匹配的。详细情况,大家可以测试一下。

相反,invoke方法没有这么严格要限制,基会根据方法句柄调用时的方法类型(这个是动态的)和其声明时的方法类型作类型转换,如果不在转换规则内,方法调用会失败。

可变参数长度的方法句柄

1、asVarargsCollector方法,它的作用是把原始方法句柄对应的方法类型的最后一个数组类型的参数转换成对应类型的可变长度参数,如下例:
public class Varargs {

public void normalMethod(String arg1, int arg2, int[] arg3) {
}

public void asVarargsCollector() throws Throwable {
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod",   MethodType.methodType(void.class, String.class, int.class, int[].class));
    mh = mh.asVarargsCollector(int[].class);
    mh.invoke(this, "Hello", 2, 3, 4, 5);
    System.out.println(mh.type());
}

} 2、asCollector方法,作用与asVarargsCollector类似,不同的是该方法只会把指定数量的参数收集到原始方法句柄对应的底层方法的数组类型参数中。如:
public void asCollector() throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod", MethodType.methodType(void.class, String.class, int.class, int[].class)); mh = mh.asCollector(int[].class, 2); mh.invoke(this, "Hello", 2, 3, 4); System.out.println(mh.type()); } 3、asSpreader方法,与上述两个方法的转换方向相返,上述两个方法是把数组类型的参数转换成长度可变的数组,而asSpreader正好相反。如下例:
public void toBeSpreaded(String arg1, int arg2, int arg3, int arg4) { } public void asSpreader() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(Varargs.class, "toBeSpreaded", MethodType.methodType(void.class, String.class, int.class, int.class, int.class)); mh = mh.asSpreader(int[].class, 3); mh.invoke(this, "Hello", new int[]{3, 4, 5}); System.out.println(mh.type()); }
4、asFixedArity方法,把参数长度可变的方法,转换为参数长度不可变的方法。如:
public void varargsMethod(String arg1, int... args) { } public void asFixedArity() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(Varargs.class, "varargsMethod", MethodType.methodType(void.class, String.class, int[].class)); mh = mh.asFixedArity(); mh.invoke(this, "Hello", new int[]{2, 4}); System.out.println(mh.type()); }

参数绑定

如前示例所示,如果方法句柄调用的非静态方法,则在调用时要提供一个方法接收者。这个接收者可以在invoke时同时指定,也可以通过bindTo方法指定。另外一点要注意的,bindTo只是绑定方法句柄的第一个参数,bindTo可以多次调用,依次绑定相应的参数。如: public void multipleBindTo() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(String.class, "indexOf", MethodType.methodType(int.class, String.class, int.class)); mh = mh.bindTo("Hello").bindTo("l"); System.out.println(mh.invoke(2));

获取方法句柄

前面的示例都是通过MethodHandles.Lookup查找类来获取方法句柄的,除了MethodType抽象出来了之外,其查找底层方法与反射也基本类似,但不同的是,方法句柄并不区分构造方法,方法,域等,而是统一转换成MethodHandle。Lookup提供了不同的查找方法,如下例:

public void lookupMethod() throws NoSuchMethodException, IllegalAccessException { MethodHandles.Lookup lookup = MethodHandles.lookup(); //构造方法 lookup.findConstructor(String.class, MethodType.methodType(void.class, byte[].class)); //String.substring lookup.findVirtual(String.class, "substring", MethodType.methodType(String.class, int.class, int.class)); //String.format lookup.findStatic(String.class, "format", MethodType.methodType(String.class, String.class, Object[].class)); } 另外,Lookup也提供查找私有方法的工厂方法,findSpecial,调用此方法获取底层私有方法的句柄时,要符合JVM的访问控制要求,进行方法查找的类要具备访问私有方法的权限,也就是说方法句柄是在查找方法时进行访问控制校验的,而不是像反射是在执行时。另外,findSpecial方法参数也比之前几种多一个,这个参数用来指定私有方法被调用时所使用的类,这个类要有对这个私有方法的访问权限,否则将出错。

public MethodHandle lookupSpecial() throws NoSuchMethodException, IllegalAccessException, Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findSpecial(MethodHandleLookup.class, "protectedMethod", MethodType.methodType(void.class), MethodHandleLookup.class); System.out.println(mh.type()); mh.invoke(this); return mh; } 另个Lookup还提供了一些setter、getter方法的查找,还可以通过反射API得到的构造器、方法、域等信息查找相应的方法句柄,同学们可以自行研究。

其它

方法句柄还有其它很多灵活的使用方法,有待大家发掘,如:方法句柄变换,MethodHands提供了丰富的api可以对原方法句柄进行相应的变换。如:dropArguments,insertArguments,filterAgrements等,还有很多。大家可以自行研究。 MethodHand的invoke方法,有一个版本可以传递另一个方法句柄作为参数,通过此功能可以完成责任链的处理模式。甚至功能更强。
方法句柄还可以实接口、实现函数式编程等。所有这些功能大家可以自己研究一下。

方法句柄与反射的性能对比

public class ContrastMethodHandleAndRelection {

final static Long COUNT = 100000L;
//-XX:CompileThreshold=500  -XX:+PrintCompilation
public static Object reflection(Method method) throws Exception {
    int result = (int)method.invoke("abc");
    return Math.multiplyExact(result,Long.BYTES);
}

public static Object methodHandle(MethodHandle mh) throws Throwable{
    int result = (int) mh.invoke("abc");
    return Math.multiplyExact(result,Long.BYTES);
}

public static void main(String[] args) throws Throwable{
    String methodName = "length";
    Method method = String.class.getDeclaredMethod(methodName);

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodType type = MethodType.methodType(int.class);
    MethodHandle mh = lookup.findVirtual(String.class, methodName, type);
    //预热
    for(int i=0 ;i<COUNT; i++){
        reflection(method);
        methodHandle(mh);
    }

    long time = System.currentTimeMillis();
    for(int i=0 ;i<COUNT; i++){
        reflection(method);
    }
    System.out.println(COUNT +" times Reflection spent ["+(System.currentTimeMillis()-time)+"]ms");

    time = System.currentTimeMillis();
    for(int i=0 ;i<COUNT; i++){
        methodHandle(mh);
    }
    System.out.println(COUNT +" times MethodHandle spent ["+(System.currentTimeMillis()-time)+"]ms");
}

} 上述示例分别设置JVM参数-Xint以解释执行方式,和-server,默认JIT阈值为10000,运行看结果,经果发现解释执行的结果方法句柄方式比反射快将近2倍,以JIT方法运行,反射也略比方法句柄方式慢一点。不明显。

java8中的invokedynamic

java7中提供了invokedynamic指令,更多的是为动态语言提供一种运行机制,java7本身并没有直接提供相应的方法生成invokedynamic字节码,需要借助asm这个字节码框架结合bootstrap和CallSite来间接实现,这里不介绍详细实现方式,有兴趣的同学baidu一下吧。下面介绍一个java8中怎么使用invokedynamic指令。
@FunctionalInterface interface Func { public boolean func(String str); } public class Lambda {
public void lambda(Func func){ return; } public static void main(String[] args) { Lambda lambda = new Lambda(); lambda.lambda(s->{return true;}); lambda.lambda(s->{return true;}); } }
上述示例字义了一个函数接口,通过@FunctionalInterface注解标注,此接口的特征是只能一个接口方法,可以包括0个或多个default接口方法。用于Lambda表达式的接口有且只能有一个接口方法。下面看一下Lambda编译之后对应的字节码。 图4

图4中1和2处对应Lambda类中两处lambda方法的调用中的lambda表达式。可以看出,编译器对每一处的lambda表达式的调用都会生成一个invokedynamci指令。其对应的#4和#6常量池如下: 图5

忽略#5,4和6号常量池对应的类型为InvokeDynamic,其后的#0和#1,代表的是BootstrapMethods属性表中的第0项和第1项。下面为这BootstrapMethods属性表的内容: 图6

我们以第0项为例介绍,第1项跟0项一样。 跟第0项相关的常量池编号为#33,#34和#35,内容如下: 图7

其中33是invokedynamic指令所指定的bootstrap方法,编译器置入,java7中要自己提供一个这种静态方法,由asm工具写入字节码中,java8中jdk提供了这样的一个启动方法。JVM在类加载解析时,如果是invokedynamic时,每次都会进行重新解析,解析的时候,会首先执行bootstrap方法,LambdaMetafactory.metafactory方法的前三个参数,会在运行时根据访问类和运行期常量池动态传入,而后三个参数,则为bootstrap静态参数列表传入。 34和35一个是方法类型参数,一个是方法句柄对象,这两个分别作为bootstrap方法的参数传入到LambdaMetafactory.metafactory方法中。感兴趣的可以debug看一下。
顺着#34继续查看,其常量类型为MethodType,发现其对应的#24为UTF8类型,这个就是方法类型的描述,对应到源码中就是Func.func(String s)方法,只是这个方法类型描述仅有参数和返回值,不包括方法名。 查看#35常量池,其对应的是invokestatic指令,35号常量池内容如下: 图8

根据java字节码规范,CONSTANTMethodHandleinfo常量池的结构包括:referent_kind,这个代表方法句柄的类型,范围必须为1-9,字由方法句柄的字节码行为决定。图7中其取值为6(invokestatic),剩下的8种有兴趣的可以查一下字节码规范,它们分别应对不同的方法句柄类型,如构造器等。#35号常量池引用#6和#41号常量池: 图9 图10

我们发现在图4中第2处也是引用的#6常量池,只不过对应的指令为invokedynamic,而图8的#35和#38常量池后面也有一个#6的参数,这个参数并不表示每6个常量池,而是CONSTRANTMethodHandleinfo的reference_kind的值为6,表示的是invokestatic指令图4的#6常量池的#1引用的是bootstrapMethods属性表中的第1项。 使用javap –v –p Lambda.class查看字节码,发现其中有两个静态的private的方法,如下: 图11

这两个方法就是编译器对lambda表达式生成的相应方法,另外,这两个方法都是private的,也就是只能在此类内部访问。我们知道,方法句柄有一个启动方法,这个方法返回一个调用点,这个调用点引用一个MethodHandler,也就是说最终对这两个private类型的lambda方法的调用在Lambda类之外,这违反了jvm安全机制。方法句柄在调用类的私有或保护类型方法时,要传入一个有访问权限的类,由此验证上面的猜测。这个类的实例就是:java/lang/invoke/LambdaMetafactory.metafactory这个方法的第一个MethodHandles.Lookup参数对应的lookupClass. 在此例中,这个类就是Lambda类。图12是Lambda进行debug时,LambdaMetafactory.metafactory方法的运行时参数值,我们可以观察到其参数值: 图12

分享到

   
搜车 React Native 依赖管理方案