方法引用(Method reference)和invokedynamic指令详细分析
invokedynamic
是jvm指令集里面最复杂的一条。本文将详细分析invokedynamic
指令是如何实现方法引用(Method reference)的。
具体言之,有这样一个方法引用:
interface Encode {
void encode(Derive person);
}
class Base {
public void encrypt() {
System.out.println("Base::speak");
}
}
class Derive extends Base {
@Override
public void encrypt() {
System.out.println("Derive::speak");
}
}
public class MethodReference {
public static void main(String[] args) {
Encode encode = Base::encrypt;
System.out.println(encode);
}
}
使用javap -verbose MethodReference.class
查看对应字节码:
// 常量池
Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#27 // #0:encode:()LEncode;
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #30.#31 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#5 = Class #32 // MethodReference
#6 = Class #33 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LMethodReference;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 encode
#19 = Utf8 LEncode;
#20 = Utf8 SourceFile
#21 = Utf8 MethodReference.java
#22 = NameAndType #7:#8 // "<init>":()V
#23 = Utf8 BootstrapMethods
#24 = MethodHandle #6:#34 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;L
java/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang
/invoke/CallSite;
#25 = MethodType #35 // (LDerive;)V
#26 = MethodHandle #5:#36 // invokevirtual Base.encrypt:()V
#27 = NameAndType #18:#37 // encode:()LEncode;
#28 = Class #38 // java/lang/System
#29 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#30 = Class #41 // java/io/PrintStream
#31 = NameAndType #42:#43 // println:(Ljava/lang/Object;)V
#32 = Utf8 MethodReference
#33 = Utf8 java/lang/Object
#34 = Methodref #44.#45 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/Str
ing;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallS
ite;
#35 = Utf8 (LDerive;)V
#36 = Methodref #46.#47 // Base.encrypt:()V
#37 = Utf8 ()LEncode;
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (Ljava/lang/Object;)V
#44 = Class #48 // java/lang/invoke/LambdaMetafactory
#45 = NameAndType #49:#53 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Lj
ava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#46 = Class #54 // Base
#47 = NameAndType #55:#8 // encrypt:()V
#48 = Utf8 java/lang/invoke/LambdaMetafactory
#49 = Utf8 metafactory
// 字节码指令
public static void main(java.lang.String[]);
0: invokedynamic #2, 0 // InvokeDynamic #0:encode:()LEncode;
5: astore_1
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
13: return
// 属性
SourceFile: "MethodReference.java"
InnerClasses:
public static final #51= #50 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#25 (LDerive;)V
#26 invokevirtual Base.encrypt:()V
#25 (LDerive;)V
使用invokedynamic
指令生成encode对象,然后存入局部变量槽#1。接着获取getstatic
获取java/lang/System
类的out
字段,最后局部变量槽#1作为参数压栈,invokevirtual
虚函数调用System.out
的println
方法。
那么invokedynamic
到底是怎么生成encode对象的呢?
1.虚拟机解析
hotspot对invokedynamic
指令的解释如下:
CASE(_invokedynamic): {
u4 index = Bytes::get_native_u4(pc+1);
ConstantPoolCacheEntry* cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
// We are resolved if the resolved_references field contains a non-null object (CallSite, etc.)
// This kind of CP cache entry does not need to match the flags byte, because
// there is a 1-1 relation between bytecode type and CP entry type.
if (! cache->is_resolved((Bytecodes::Code) opcode)) {
CALL_VM(InterpreterRuntime::resolve_from_cache(THREAD, (Bytecodes::Code)opcode),
handle_exception);
cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
}
Method* method = cache->f1_as_method();
if (VerifyOops) method->verify();
if (cache->has_appendix()) {
ConstantPool* constants = METHOD->constants();
SET_STACK_OBJECT(cache->appendix_if_resolved(constants), 0);
MORE_STACK(1);
}
istate->set_msg(call_method);
istate->set_callee(method);
istate->set_callee_entry_point(method->from_interpreted_entry());
istate->set_bcp_advance(5);
// Invokedynamic has got a call counter, just like an invokestatic -> increment!
BI_PROFILE_UPDATE_CALL();
UPDATE_PC_AND_RETURN(0); // I'll be back...
}
使用invokedynamic_cp_cache_entry_at获取常量池对象,然后检查是否已经解析过,如果没有就解析反之复用,然后设置方法字节码,留待后面解释执行。那么,重点是这个解析。我们对照着jvm spec来看。
根据jvm文档的描述,invokedynamic
的操作数(operand)指向常量池一个动态调用点描述符(dynamic call site specifier)。
动态调用点描述符是一个CONSTANT_InvokeDynamic_info结构体:
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
tag
表示这个结构体的常量,不用管bootstrap_method_attr_index
启动方法数组name_and_type_index
一个名字+类型的描述字段,就像这样Object p
放到虚拟机里面表示是Ljava/lang/Object; p
然后启动方法数组结构是这样:
BootstrapMethods_attribute {
...
u2 num_bootstrap_methods;
{
u2 bootstrap_method_ref;
u2 num_bootstrap_arguments;
u2 bootstrap_arguments[num_boot]
} bootstrap_methods[num_bootstrap_methods];
}
就是一个数组,每个元素是{指向MethodHandle的索引,启动方法参数个数,启动方法参数}
MethodlHandle
是个非常重要的结构,指导了虚拟机对于这个启动方法的解析,先关注一下这个结构:
CONSTANT_MethodHandle_info {
u1 tag;//表示该结构体的常量tag,可以忽略
u1 reference_kind;
u2 reference_index;
}
- reference_kind是[1,9]的数,它表示这个method handle的类型,这个字段和字节码的行为有关。
- reference_index 根据reference_kind会指向常量池的不同类型,具体来说
- reference_kind==1,3,4 指向CONSTANT_Fieldref_info结构,表示一个类的字段
- reference_kind==5,8,指向CONSTANT_Methodref_info,表示一个类的方法
- reference_kind==6,7, 同上,只是兼具接口的方法或者类的方法的可能。
- reference_kind==9,指向CONSTATN_InterfaceMethodref_info,表示一个接口方法
通过invokedynamic
,我们可以得
- 名字+描述符的表示(由
name_and_type_index
给出) - 一个启动方法数组(由
bootstrap_method_attr_index
给出)
2.手动解析
可以手动模拟一下解析,看看最后得到的数据是什么样的。在这个例子中:
0: invokedynamic #2, 0 //第二个operand总是0
查看常量池#2
项:
#2 = InvokeDynamic #0:#27 // #0:encode:()LEncode;
#27 = NameAndType #18:#37 // encode:()LEncode;
BootstrapMethods:
0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#25 (LDerive;)V
#26 invokevirtual Base.encrypt:()V
#25 (LDerive;)V
得到的名字+描述符是:Encode.encode()
,启动方法数组有一个元素,回忆下之前说的,这个元素构成如下:
{指向MethodHandle的索引,启动方法参数个数,启动方法参数}
这里得到的MethodHandle表示的是LambdaMetafactory.metafactory:
#24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;`
启动方法参数有:
#25 (LDerive;)V
#26 invokevirtual Base.encrypt:()V
#25 (LDerive;)V
3. java.lang.invoke.LambdaMetafactory
先说说LambdaMetafactory有什么用。javadoc给出的解释是:
LambdaMetafactory方便我们创建简单的"函数对象",这些函数对象通过代理MethodHandle实现了一些接口。
当这个函数返回的CallSite被调用的时候,会产生一个类的实例,该类还实现了一些方法,具体由参数给出
将上面得到的MethodHandle写得更可读就是调用的这个方法:
public static CallSite LambdaMetafactory.metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType);
六个参数,慢慢来。
3.1 LambdaMetafactory.metafactory()
调用前
要知道参数是什么意思,可以从它的调用者来管中窥豹:
static CallSite makeSite(MethodHandle bootstrapMethod,
// Callee information:
String name, MethodType type,
// Extra arguments for BSM, if any:
Object info,
// Caller information:
Class<?> callerClass) {
MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
CallSite site;
try {
Object binding;
info = maybeReBox(info);
if (info == null) {
binding = bootstrapMethod.invoke(caller, name, type);
} else if (!info.getClass().isArray()) {
binding = bootstrapMethod.invoke(caller, name, type, info);
} else {
Object[] argv = (Object[]) info;
maybeReBoxElements(argv);
switch (argv.length) {
...
case 3:
binding = bootstrapMethod.invoke(caller, name, type,
argv[0], argv[1], argv[2]);
break;
...
}
}
//System.out.println("BSM for "+name+type+" => "+binding);
if (binding instanceof CallSite) {
site = (CallSite) binding;
} else {
throw new ClassCastException("bootstrap method failed to produce a CallSite");
}
...
} catch (Throwable ex) {
...
}
return site;
}
对java.lang.invoke.LambdaMetafactory
的调用是通过MethodHandle
引发的,所以可能还需要补一下MethodHandle
的用法,百度一搜一大堆,javadoc也给出了使用示例:
String s;
MethodType mt; MethodHandle mh;
MethodHandles.Lookup lookup = MethodHandles.lookup();
// mt is (char,char)String
mt = MethodType.methodType(String.class, char.class, char.class);
mh = lookup.findVirtual(String.class, "replace", mt);
s = (String) mh.invoke("daddy",'d','n');
// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
assertEquals(s, "nanny");
回到源码,关键是这句:
binding = bootstrapMethod.invoke(caller, name, type,
argv[0], argv[1], argv[2]);
argv[0],argv[1],argv[2]
分别表示之前启动方法的三个参数,
caller即调用者,这里是MethodReference
这个类,然后name和type参见下面的详细解释:
MethodHandles.Lookup caller
表示哪个类引发了调动String invokedName
表示生成的类的方法名,对应例子的encode
MethodType invokedType
表示CallSite的函数签名,其中参数类型表示捕获变量的类型,返回类型是类要实现的接口的名字,对应例子的()Encode
,即要生成一个类,这个类没有捕获自由变量(所以参数类为空),然后这个类要实现Encode接口(返回类型为生成的类要实现的接口)
接下来MethodType samMethodType
表示要实现的方法的函数签名和返回值,对于例子的#25 (LDerive;)V
,即实现方法带有一个形参,返回voidMethodHandle implMethod
表示实现的方法里面应该调用的函数,对于例子的#26 invokevirtual Base.encrypt:()V
,表示调用Base的虚函数encrypt,返回voidMethodType instantiatedMethodType
表示调用方法的运行时描述符,如果不是泛型就和samMethodType
一样
3.2 LambdaMetafactory.metafactory()
调用
源码面前,不是了无秘密吗hhh,点进源码看看这个LambdaMetafactory到底做了什么:
*/
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();
}
它什么也没做,做事的是InnerClassLambdaMetafactory.buildCallSite()
创建的最后CallSite,那就进一步看看InnerClassLambdaMetafactory.buildCallSite()
:
@Override
CallSite buildCallSite() throws LambdaConversionException {
// 1. 创建生成的类对象
final Class<?> innerClass = spinInnerClass();
if (invokedType.parameterCount() == 0) {
// 2. 用反射获取构造函数
final Constructor<?>[] ctrs = AccessController.doPrivileged(
new PrivilegedAction<Constructor<?>[]>() {
@Override
public Constructor<?>[] run() {
Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
if (ctrs.length == 1) {
// The lambda implementing inner class constructor is private, set
// it accessible (by us) before creating the constant sole instance
ctrs[0].setAccessible(true);
}
return ctrs;
}
});
if (ctrs.length != 1) {
throw new LambdaConversionException("Expected one lambda constructor for "
+ innerClass.getCanonicalName() + ", got " + ctrs.length);
}
try {
// 3. 创建实例
Object inst = ctrs[0].newInstance();
// 4. 根据实例和samBase(接口类型)生成MethodHandle
// 5. 生成ConstantCallSite
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
}
catch (ReflectiveOperationException e) {
throw new LambdaConversionException("Exception instantiating lambda object", e);
}
} else {
try {
UNSAFE.ensureClassInitialized(innerClass);
return new ConstantCallSite(
MethodHandles.Lookup.IMPL_LOOKUP
.findStatic(innerClass, NAME_FACTORY, invokedType));
}
catch (ReflectiveOperationException e) {
throw new LambdaConversionException("Exception finding constructor", e);
}
}
}
首先它生成一个.class文件,虚拟机默认不会输出,需要下面设置VM option-Djdk.internal.lambda.dumpProxyClasses=.
,Dump出虚拟机生成的类我得到的是:
import java.lang.invoke.LambdaForm.Hidden;
// $FF: synthetic class
final class MethodReference$$Lambda$1 implements Encode {
private MethodReference$$Lambda$1() {
}
@Hidden
public void encode(Derive var1) {
((Base)var1).encrypt();
}
}
该类实现了传来的接口函数(动态类生成,熟悉spring的朋友应该很熟悉)。
回到buildCallSite()源码,它使用MethodHandles.constant(samBase, inst)
创建MethdHandle,放到CallSite里面,完成整个LambdaMetafactory的工作。MethodHandles.constant(samBase, inst)
相当于一个总是返回inst的方法。
总结
到这里就结束了整个流程,文章有点长,总结一下:
- 虚拟机遇到invokedynamic,开始解析操作数
- 根据
invokedynamic #0:#27
获取到启动方法(#0)和一个名字+描述符
(#27)
其中启动方法是
BootstrapMethods:
0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#25 (LDerive;)V
#26 invokevirtual Base.encrypt:()V
#25 (LDerive;)V
名字+描述符
是
#27 = NameAndType #18:#37 // encode:()LEncode;
- 启动方法指向LambdaMetafactory.metafactory,但是不会直接调用而是通过MethdHandle间接调用。调用位置位于CallSite.makeCallSite()
LambdaMetafactory.metafactory()
其实使用InnerClassLambdaMetafactory.buildCallSite()
创建了最后的CallSite- buildCallSite()会创建一个.class,
- buildCallSite()会向最后的CallSite里面放入一个可调用的MethdHandle
- 这个MethodHandle指向的是一个总是返回刚刚创建的.class类的实例的方法,由
MethodHandles.constant(samBase, inst)
完成 - 最后,用invokevirtual调用CallSite里面的MethdHandle,返回.class类的示例,即
inst
,即new MethodReference$$Lambda$1