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