Java与动态类型

在JDK7以前,Java虚拟机层面对动态语言的支持一直有所欠缺。这是因为方法调用的4个指令:invokevirtual、invokespecial、invokestatic、invokeinterface的第一个参数都是被调用方法的符号引用。方法的符号引用在编译时产生,而动态语言只能在运行期确定方法的接收者。我们可以思考一下,如何基于现有的技术来实现动态语言的支持?JVM可以在编译时将方法的所有版本记录并缓存,在运行时逐个遍历直到找到合适的方法版本,然而这种“曲线救国”方式将会大大增加内存开销,得不偿失。因此要支持动态语言类型只能从JVM虚拟机层面解决。
到了JDK7版本,为了支持动态类型,在JVM层面新增了一个方法调用指令:invokedynamic指令,新增了一个工具包:java.lang.invoke包。
接下来我们就来揭开这两个东西的神秘面纱。

invokedynamic指令

lambda表达式是Java语言支持动态语言的一项革命性特性,在lambda表达式出现之前,我们可以通过匿名内部类的方式实现lambda表达式的功能,因此很多教科书或博客将lambda表达式解读为匿名内部类的一种优化和升级。从功能实现的角度来看这种说法无可厚非,但从思想认知的角度来看,这种观点是错误的。原因在于匿名内部类和lambda表达式的实现思想是完全不一样。下面我们通过实例代码来揭开lambda表达式与invokedynamic指令的关系。

package com.leon.util;

public class Test {

    public interface OperationInterface {
        int add(int x, int y);
    }

    public static void main(String[] args) {
        OperationInterface opt = ((x, y) -> x * y);
        int result = opt.add(1, 2);
        System.out.println(result);
    }
}

输出结果:
2

以上代码是一个简单的Lambda表达式的应用,对于结果显然没有疑问。我们更关心的是它是如何运行的。从反编译结果来一探究竟:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:add:()Lcom/leon/util/Test$OperationInterface;
         5: astore_1
         6: aload_1
         7: iconst_1
         8: iconst_2
         9: invokeinterface #3,  3            // InterfaceMethod com/leon/util/Test$OperationInterface.add:(II)I
        14: istore_2
        15: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
        line 14: 0
        line 15: 6
        line 16: 15
        line 17: 22
}
SourceFile: "Test.java"
InnerClasses:
     public static #9= #8 of #6; //OperationInterface=class com/leon/util/Test$OperationInterface of class com/leon/util/Test
     public static final #51= #50 of #53; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #23 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodH
andle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #24 (II)I
      #25 invokestatic com/leon/util/Test.lambda$main$0:(II)I
      #24 (II)I

以上是main方法的反编译结果,和Class文件部分额外的信息。在main方法的编译结果的0行出现了invokedynamic指令!invokedynamic指令的入参是常量池中#2所代表的常量池和0所代表的BootstrapMethods属性。

#2 = InvokeDynamic      #0:#26         // #0:add:()Lcom/leon/util/Test$OperationInterface;

BootstrapMethods:
  0: #23 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodH
andle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;

BootstrapMethods属性中,通过invokestatic指令调用了java.lang.invoke包中的LambdaMetafactory.metafactory()方法,并返回java.lang.invoke.CallSite。

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

我们暂时不探讨java.lang.invoke包的具体实现,在这里我们只需知道,最终完成lambda表达式的解析是通过java.lang.invoke包完成的!
invokedynamic指令到底扮演的啥角色呢?
在Class文件中,每一处invokedynamic指令的位置都被称为“动态调用点(Dynamic-Computed Call Site)”
通过invokedynamic指令我们能够获取到3点信息:
1.调用方法的名称和描述符;
2.在引导方法属性中定义的该lambda表达式对应的引导方法执行;(引导方法属性中可以有很多引导方法,只是本例只有这一个而已);
3.引导方法的入参和出参类型。
在运行期,通过该指令就能够找到对应的方法版本并执行。
到这里,通过invokedynamic指令完成的方法动态调用解析就完成了。但迷雾还没有真正的揭开:java.lang.invoke包。

java.lang.invoke包

这个包是JDK7加入的。而它的主要目的是提供一种新的确定目标方法调用的机制,称为“方法句柄”。
在这之前,确定方法调用的版本只能基于符号引用。
方法句柄代码示例:

package com.leon.util;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
 * @author created by leon on 2020-05-15
 * @since v1.0
 */
public class Test {

    static class MyPrintClassA {
        public void println(String str) {
            System.out.println(str);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new MyPrintClassA();
        // 获取指定方法的方法句柄。
        MethodHandle methodHandle = getMethodHandle(obj, "println");
        // 调用
        methodHandle.invoke("Hello, man");
    }

    public static MethodHandle getMethodHandle(Object receiver, String methodName) throws NoSuchMethodException, IllegalAccessException {
        // MethodType代表方法的类型,第一个参数是出参类型,第二个以及之后的代表入参类型。
        MethodType methodType = MethodType.methodType(void.class, String.class);
        // Lookup是MethodHandles的内部类,主要是根据指定的参数查找对应的方法。
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // 根据JVM方法调用规则,println属于虚方法,因此应当调用findVirtual()方法,
        // 传入类型、方法名称、即对应的方法类型,获取到方法的句柄对象:MethodHandle
        MethodHandle methodHandle = lookup.findVirtual(receiver.getClass(), methodName, methodType);
        // 每个虚方法的入参都有一个隐式的参数:this,在这里我们需要通过bindTo()方法显式的绑定“this”对象。
        return methodHandle.bindTo(receiver);
    }
}

执行结果:

Hello, man

以上示例实际上就是模拟了invokeDynamic指令的执行过程。只不过我们是通过Java代码实现的而已。通过获取到方法句柄,就可以实现对方法的调用。
其实,同样的事情反射也可以做到。
站在使用的角度来看,反射和方法句柄机制都是在模拟方法的调用。但是他们的实现思想有着本质的区别:
1.反射是在Java代码层面模拟方法的调用,而方法句柄则是在字节码层面模拟方法的反射调用;
2.java.lang.reflect.Method对象包含的方法信息比java.lang.MethodHandle对象包含的方法信息多得多,Method对象包含了方法的描述符、签名、方法属性表中的所有属性等,而MethodHandle方法包含的仅仅是方法的基本信息;
3.从服务范围的角度来看,反射仅服务于Java语言,而java.lang.invoke包则服务于所有建立在JVM虚拟机上的语言。

通过对对invokeDynamic指令的剖析,进而对java.lang.invoke包进行了简单的应用和介绍。从而明白了Java是如何实现对动态类型的支持。关于java.lang.invoke包的详细说明和介绍,网络上有比较详细的介绍。这里不过多的介绍其使用方式。

你可能感兴趣的:(深入理解JVM虚拟机)