方法引用(Method reference)和invokedynamic指令详细分析
方法引用(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给出的解释是:
Facilitates the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle, after appropriate type adaptation and partial evaluation of arguments. Typically used as a bootstrap method for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.
When the target of the 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 invokedType, declares a method with the name given by invokedName and the signature given by samMethodType. It may also override additional methods from Object.
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
方法引用(Method reference)和invokedynamic指令详细分析的更多相关文章
- Java中的函数式编程(四)方法引用method reference
写在前面 我们已经知道,lambda表达式是一个匿名函数,可以用lambda表达式来实现一个函数式接口. 很自然的,我们会想到类的方法也是函数,本质上和lambda表达式是一样的,那是否也可以用类 ...
- java 方法引用(method reference)
it -> it != null等价于Objects::nonNull
- Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针
Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针 1.1. java方法引用(Method References) 与c#委托与脚本语言js ...
- JAVA 8 方法引用 - Method References
什么是方法引用 简单地说,就是一个Lambda表达式.在Java 8中,我们会使用Lambda表达式创建匿名方法,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对 ...
- Java 8Lambda之方法引用(Method References)
方法引用分为4类,方法引用也受到访问控制权限的限制,可以通过在引用位置是否能够调用被引用方法来判断.具体分类信息如下: 类型 使用方式 静态方法 ContainingClass::staticMeth ...
- 方法引用(Method References)
* 方法引用的使用 * * 1.使用情境:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用! * * 2.方法引用,本质上就是Lambda表达式,而Lambda表达式作为函数式接口 ...
- 30分钟入门Java8之方法引用
30分钟入门Java8之方法引用 前言 之前两篇文章分别介绍了Java8的lambda表达式和默认方法和静态接口方法.今天我们继续学习Java8的新语言特性--方法引用(Method Referenc ...
- Java8之方法引用
一.概述 在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法.然而,有时候我们仅仅是调用了一个已存在的方法.如下: Arrays.sort(stringsArray,(s1,s ...
- Lambda学习总结(三)--方法引用
一.方法引用 1.1 方法引用含义 在学习了 Lambda 表达式之后,我们通常会使用 Lambda 表达式来创建匿名方法.但有的时候我们仅仅是需要调用一个已存在的方法.如下示例: @Function ...
随机推荐
- cocoapods使用问题集锦(2017-04)
今天公司在公司新发的电脑上边安装cocoapod发现容易忘记的几个问题,感觉需要记录下来. 问题一:系统默认ruby镜像的卸载命令行 --> gem sources --remove h ...
- 28-组合数(dfs)
http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=32 组合数 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 ...
- 空值和null区别
空值代表杯子是真空的,NULL代表杯子中装满了空气
- 为啥final类型的map或者arraylist可以修改数据 而final类型的String变量不可以修改数据呢
比如 final Map map =new HashMap(); 可以往map里put数据final List list =new ArrayList(); 可以往list里 ...
- UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 12: ordinal not in range(128)问题解决
今天在验证字符串是否包含的时候报错:UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 12: ordinal n ...
- [GO]并的爬取捧腹的段子
package main import ( "fmt" "strconv" "net/http" "regexp" &q ...
- yii2 gridview 新增按钮 动态显示按钮
新增一个按钮 [ 'class' => 'yii\grid\ActionColumn', 'header' => '操作', 'options' => ['width' => ...
- javascript总结37:DOM:innerText 和 innerHTML
innerText 和 innerHTML 作用: 给双标签的元素设置内容/获取双标签里面的内容 不同点: innerText 只是用于获取文本或设置文本 innerHTML 不仅可以用于设置/获取文 ...
- TCP三次握手与防火墙规则
一个(tct)socket连接需要在客户端与服务端开启一个隧道,客户端提供一个端口(new时可指定,也可不指定,随机),服务端的端口和地址一定要指定.在win下,服务端创建监听端口时,防火墙会提示阻止 ...
- Linux Guard Service - 守护进程分裂
分裂守护进程 由于fork()后第一行仍然在循环中,使用fork()返回值鉴别当前进程的性质 int i = 0; for (i = 0; i < 10; i++) { // sleep(1); ...