《深入理解Java虚拟机》
《深入理解Java虚拟机》原文:
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期。
这里有几个名词需要说下:
java是静态类型语言,虽然java存在静态类型(外观类型)和实际类型的概念。这里的实际类型虽然是运行期间可知的(编译期间可能不可知),但java中的每个变量都要有静态类型的。比如obj的静态类型为java.lang.Object,则obj的实际类型就必须是Object的子类(或本身)才是合法的。比如:
Object obj = new ArrayList();
相比较java的静态类型语言,动态类型语言中的变量在定义的时候是不需要指定变量的静态类型的。比如:
var va = "abc";
静态类型语言在编译期间会做一些验证工作。比如java的如下代码就是在编译期间报的错:
Object obj = new Object();
obj.xxx();//报错
java语言会在编译期间需要生成xxx()方法的符号引用, 如果找不到对应的方法,编译器就会报错。但如果把相似的代码换成动态类型语言就不会报错,代码如下:
var a = 1243;
a.xxx();
java虚拟机层面为了支持动态类型,引入了invokedynamic指令以及java.lang.invoke包。
引入java.lang.invoke这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为MethodHandle。
下面演示了MethodHandle的基本用途,无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确地调用到println()方法。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleTest {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
Object object = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getPrintlnMH(object).invokeExact("zzc");
}
private static MethodHandle getPrintlnMH(Object reveiver) throws NoSuchMethodException, IllegalAccessException {
//MethodType:代表"方法类型",包含了方法的返回值(methodType的第一个参数)和具体参数methodType第二个及以后的参数
MethodType methodType = MethodType.methodType(void.class, String.class);
//lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄
MethodHandle println = MethodHandles.lookup().findVirtual(reveiver.getClass(), "println", methodType);
//因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象,
// 这个参数以前是放在参数列表中进行传递的,而现在提供了bindTo()方法来完成这件事情
return println.bindTo(reveiver);
}
}
上面代码中的getPrintlnMH()
方法模拟了invokevirtual指令的执行过程,且包含invokevirtual指令中的符号引用,即java编译期间做的工作。只不过它的分派逻辑并非固化在class文件的字节码上,而是通过一个具体方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。以此为基础,有了MethodHandle就可以写出类似于下面这样的函数声明:
void sort(List list,MethoHandle compare);
上面的代码依赖于Reflection API,但这个API是为Java语言服务的,而MethodHandle方法调用方式是服务于所有java虚拟机之上的语言,java语言只是其中之一。
invokedynamic指令的目的和上面的代码是一致的,即把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码中,让用户(包含其他语言的设计者)有更高的自由度。相比较上面的代码,只不过一个采用上层java代码和API来实现,另一个用字节码和Class中其他属性、常量来完成。
invokedynamic指令的语法构成:
invokedynamic + CONSTANT_InvokeDynamic_info
CONSTANT_InvokeDynamic_info = 引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethod属性中) +方法类型(MethodType)+方法名称
引导方法
引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,该对象代表真正要执行的目标方法。
invokedynamic执行流程
根据CONSTANT_InvokeDynamic_info
常量中提供的信息,虚拟机可以找到引导方法,并执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。
通过例子解释这个过程
@FunctionalInterface
interface FunctionDemo<T, R> {
void apply(T var1);
}
public class DynamicInvokeTest {
private void say(FunctionDemo functionDemo, int x) {
functionDemo.apply(x);
}
public static void main(String[] args) {
DynamicInvokeTest dynamicInvokeTest = new DynamicInvokeTest();
FunctionDemo functionDemo = (tmp) -> {
System.out.println(tmp);
};
dynamicInvokeTest.say(functionDemo, 100);
}
}
要运行上面的代码,jdk版本要大于1.7,因为用到了lamda表达式,lamda表达式就是借助invokedynamic
指令实现的。
上面代码的字节码如下:
Constant pool:
#6 = InvokeDynamic #0:#46 // #0:apply:()Lcom/zzc/rtti/FunctionDemo;
#46 = NameAndType #57:#60 // apply:()Lcom/zzc/rtti/FunctionDemo;
{
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: new #4 // class com/zzc/rtti/DynamicInvokeTest
3: dup
4: invokespecial #5 // Method "":()V
7: astore_1
8: invokedynamic #6, 0 // InvokeDynamic #0:apply:()Lcom/zzc/rtti/FunctionDemo;
13: astore_2
14: aload_1
15: aload_2
16: bipush 100
18: invokespecial #7 // Method say:(Lcom/zzc/rtti/FunctionDemo;I)V
21: return
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 args [Ljava/lang/String;
8 14 1 dynamicInvokeTest Lcom/zzc/rtti/DynamicInvokeTest;
14 8 2 functionDemo Lcom/zzc/rtti/FunctionDemo;
}
SourceFile: "DynamicInvokeTest.java"
InnerClasses:
public static final #72= #71 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #43 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/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#44 (Ljava/lang/Object;)V
#45 invokestatic com/zzc/rtti/DynamicInvokeTest.lambda$main$0:(Ljava/lang/Object;)V
#44 (Ljava/lang/Object;)V
补充
执行javap -c -p DynamicInvokeTest.class
查看类的私有方法。
private static void lambda$main$0(java.lang.Object);
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
7: return
从main()方法的字节码可见,invokedynamic指令的第一个参数为第6项常量(第二个参数当前没有用,是个占位符)。
从常量池可以看到#6的显示的是#6 = InvokeDynamic #0:#46
,常量值中的#0
表示引导方法取BootstrapMethods属性表的第0项。而后面的#46
是一个CONSTANT_NameAndType_info
变量,从这个常量中可以获取方法名称和描述符,即:apply:()Lcom/zzc/rtti/FunctionDemo;
观察BootstrapMethods属性表中第0项,即引导方法。可以看到引导方法是LambdaMetafactory类中的metafactory方法,该方法源码如下:
/**
* Facilitates the creation of simple "function objects" that implement one
* or more interfaces by delegation to a provided {@link MethodHandle},
* after appropriate type adaptation and partial evaluation of arguments.
* Typically used as a bootstrap method for {@code invokedynamic}
* call sites, to support the lambda expression and method
* reference expression features of the Java Programming Language.
*
* This is the standard, streamlined metafactory; additional flexibility
* is provided by {@link #altMetafactory(MethodHandles.Lookup, String, MethodType, Object...)}.
* A general description of the behavior of this method is provided
* {@link LambdaMetafactory above}.
*
*
When the target of the {@code CallSite} returned from this method is
* invoked, the resulting function objects are instances of a class which
* implements the interface named by the return type of {@code invokedType},
* declares a method with the name given by {@code invokedName} and the
* signature given by {@code samMethodType}. It may also override additional
* methods from {@code Object}.
*
* @param caller Represents a lookup context with the accessibility
* privileges of the caller. When used with {@code invokedynamic},
* this is stacked automatically by the VM.
* @param invokedName The name of the method to implement. When used with
* {@code invokedynamic}, this is provided by the
* {@code NameAndType} of the {@code InvokeDynamic}
* structure and is stacked automatically by the VM.
* @param invokedType The expected signature of the {@code CallSite}. The
* parameter types represent the types of capture variables;
* the return type is the interface to implement. When
* used with {@code invokedynamic}, this is provided by
* the {@code NameAndType} of the {@code InvokeDynamic}
* structure and is stacked automatically by the VM.
* In the event that the implementation method is an
* instance method and this signature has any parameters,
* the first parameter in the invocation signature must
* correspond to the receiver.
* @param samMethodType Signature and return type of method to be implemented
* by the function object.
* @param implMethod A direct method handle describing the implementation
* method which should be called (with suitable adaptation
* of argument types, return types, and with captured
* arguments prepended to the invocation arguments) at
* invocation time.
* @param instantiatedMethodType The signature and return type that should
* be enforced dynamically at invocation time.
* This may be the same as {@code samMethodType},
* or may be a specialization of it.
* @return a CallSite whose target can be used to perform capture, generating
* instances of the interface named by {@code invokedType}
* @throws LambdaConversionException If any of the linkage invariants
* described {@link LambdaMetafactory above}
* are violated
*/
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();
}
通过BootstrapMethods属性表,可以看到只会给这个方法传递三个参数,且通过上面metafactory
方法的源码注释可以知道,该方法的前三个参数是VM自动填充的,后面三个参数才是传递过去的,这三个参数如下所示:
#44 (Ljava/lang/Object;)V
#45 invokestatic com/zzc/rtti/DynamicInvokeTest.lambda$main$0:(Ljava/lang/Object;)V
#44 (Ljava/lang/Object;)V
通过metafactory
方法源码可以知道,该方法会返回一个CallSite
,该对象就是真正要执行的目标方法,通过字节码,可以看到在执行完invokedynamic指令后,执行了astore_2
命令,即CallSite
对象被放到第三个本地变量中,在调用say
方法之前被压入操作栈中,通过以上操作可以说明invokedynamic
指令执行完成之后,返回的是FunctionDemo
的实现类。
那这个FunctionDemo
的实现类到底是啥,可以通过以下命令执行上面的代码:
java -Djdk.internal.lambda.dumpProxyClasses com.zzc.rtti.DynamicInvokeTest
该命令把内存中生成的类保存下来,执行后我们可以看到这样一个class文件:
DynamicInvokeTest$$Lambda$1.class
通过javap反编译该文件:
F:\Study\Springboot\common-art\demo\target\classes\com\zzc\rtti>javap -c DynamicInvokeTest$$Lambda$1.class
final class com.zzc.rtti.DynamicInvokeTest$$Lambda$1 implements com.zzc.rtti.FunctionDemo {
public void apply(java.lang.Object);
Code:
0: aload_1
1: invokestatic #18 // Method com/zzc/rtti/DynamicInvokeTest.lambda$main$0:(Ljava/lang/Object;)V
4: return
}
可以看到,该类继承FunctionDemo
接口,调用的方法是,编译器在DynamicInvokeTest
中自动生成的lambda$main$0()
方法,其实这个方法就是按照我们Lambda表达式的内容自动生成的。可以看出Lambda表达式也是通过内部类实现的。
言归正传,invokedynamic
指令获得FunctionDemo
的实现类,然后把该类作为参数调用say
方法,say方法的调用流程走完。