Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

LambdaMetafactory 类详解

昨天看了 fastjson 作者一篇 《用LambdaMetafactory生成函数映射代替反射提升性能》 的文章,其中主要介绍了 fastjson中如何使用 LambdaMetafactory来生成函数映射代替反射调用。
了解了下 LambdaMetafactory 还是比较复杂的,所以调研一下做个记录,后续写框架应该会用到。

背景

为什么使用函数映射,其主要的价值还是在于源文章中的结论:函数映射在多次调用中的性能远高于反射。这里强调了多次调用,原因是生成方法函数映射的时间消耗是远高于反射获取方法的。
平均耗时对比:

Benchmark Mode Cnt Score Error Units
genMethod(反射获取方法) avgt(平均耗时) 5 0.125 0.015 us/op
genLambda(生成方法的函数映射) avgt 5 51.880 40.04 us/op

但是生成的函数是可以复用的,将一个固定签名的函数缓存起来,可以省去后续调用再去创建函数。而函数调用的效率是远高于反射调用的(函数映射调用接近于直接方法调用)。

函数映射的生成

明确了函数映射的价值,下面我们看下如何生成函数映射:生成函数映射需要用到 jdk1.8 新增的类 LambdaMetafactorymetafactory方法。其方法包含六个参数

  • MethodHandles.Lookup caller
  • String invokedName
  • MethodType invokedType
  • MethodType samMethodType
  • MethodHandle implMethod
  • MethodType instantiatedMethodType

先看下使用的例子,在详细讲下这六个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class LambdaMetafactoryTest {

public static class LambdaMetafactoryBean {

String name;

public LambdaMetafactoryBean() {
}

public LambdaMetafactoryBean(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String say(String words) {
return "hi " + name + ", " + words;
}
}

public static void main(String[] args) {

LambdaMetafactoryBean bean = new LambdaMetafactoryBean("lucy");
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.findVirtual(LambdaMetafactoryBean.class, "say", MethodType.methodType(String.class, String.class));

CallSite callSite = LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(BiFunction.class),
MethodType.methodType(Object.class, Object.class, Object.class),
methodHandle,
methodHandle.type()
);

BiFunction func = (BiFunction<LambdaMetafactoryBean, String, String>) callSite.getTarget().invokeExact();
System.out.println(func.apply(bean, "nice to meet you."));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

输出

1
hi lucy ,nice to meet you.

讲参数之前,需要先看下MethodTypeMethodHandle 这几个参数类型。

参数类型

MethodType

MethodType 是java1.7 新增的用于描述方法类型的类。
它可以描述方法的返回类型与参数类型。所有的类型都是用其Class表示,使用Void.class代表没有返回类型。
MethodType的实例只能由其工厂方法创建,工厂方法可能是有缓存的,但并不保证。创建的实例都是不可变的(immutable)。
常用方法包含:

  • methodType()方法:用于创建一个新的MethodType对象,参数为方法的返回类型和参数类型。
  • changeReturnType()方法:用于改变MethodType对象的返回类型。
  • changeParameterType()方法:用于改变MethodType对象的参数类型。
  • insertParameterTypes()方法:用于在MethodType对象中插入一个或多个新的参数类型。
  • erase()方法:用于返回一个擦除了泛型信息的MethodType对象。

最重要的还是methodType()方法,可以通过它创建一个MethodType的实例。比如示例中的方法签名:

1
2
3
public String say(String words) {
return "hi " + name + ", " + words;
}

使用MethodType描述就是

1
MethodType.methodType(String.class, String.class)

第一个参数代表返类型,后续的参数代表参数类型。

MethodType可以用来创建方法句柄(MethodHandle)对象,这个方法句柄可以在运行时动态地调用一个指定的方法。

MethodHandle

MethodHandle 是java1.7 引入的对基础方法、构造函数、字段或类似的低级操作的类型化的、可直接执行的引用。
MethodHandle 的实例化是通 MethodHandles.Lookup的工厂方法创建的。
Lookup的工厂方法如下:

lookup expression member bytecode behavior
lookup.findGetter(C.class,”f”,FT.class) FT f; (T) this.f;
lookup.findStaticGetter(C.class,”f”,FT.class) static FT f; (T) C.f;
lookup.findSetter(C.class,”f”,FT.class) FT f; this.f = x;
lookup.findStaticSetter(C.class,”f”,FT.class) static FT f; C.f = arg;
lookup.findVirtual(C.class,”m”,MT) T m(A*); (T) this.m(arg*);
lookup.findStatic(C.class,”m”,MT) static T m(A*); (T) C.m(arg*);
lookup.findSpecial(C.class,”m”,MT,this.class) T m(A*); (T) super.m(arg*);
lookup.findConstructor(C.class,MT) C(A*); new C(arg*);
lookup.unreflectGetter(aField) (static)? FT f; (FT) aField.get(thisOrNull);
lookup.unreflectSetter(aField) (static)? FT f; aField.set(thisOrNull, arg);
lookup.unreflect(aMethod) (static)? T m(A*); (T) aMethod.invoke(thisOrNull, arg*);
lookup.unreflectConstructor(aConstructor) C(A*); (C) aConstructor.newInstance(arg*);
lookup.unreflect(aMethod) (static)? T m(A*); (T) aMethod.invoke(thisOrNull, arg*);

通过这些工厂方法(与 MethodType),可以直接创建 MethodHandle 实例。MethodHandle 的访问控制校验是在 Lookup的时候完成的,所以在调用的时候就不会再进行访问权限检验,这就使得其性能会优于反射调用。
(_实际上MethodHandle需要是static final类型的才能大幅提升性能,否则性能提升并不大。_)

最常用的MethodHandle的调用方法 1)invoke 2)invokeExact 。两个方法不同点在于invokeExact是严格匹配方法类型的,而invoke方法允许更加松散的调用方式,它会尝试在调用的时候进行返回值和参数类型的转换工作。

综上所述使用 MethodHandle只需要4步

  1. 创建lookup
  2. 创建method type
  3. 找到method handle
  4. 调用方method handle

例子

1
2
3
4
5
6
7
8
9
10
11
12
// 测试类
BeanA beanA= new BeanA("id1","aaa","vvv",18);
// 1 创建lookup
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 创建method type
MethodType methodType = MethodType.methodType(String.class, String.class);
// 找到method handle
MethodHandle methodHandle = lookup.findVirtual(BeanA.class, "sayHi", methodType);
// 调用方method handle
// Object result = methodHandle.invoke(beanA,"args");
String result =(String) methodHandle.invokeExact(beanA,"args");
System.out.println(result);

更详细的说明参见 Method Handles in Java

CallSite

CallSite 代表一个方法调用点,是Java1.7引入的的一个新特性,为了支持动态语言的实现而设计的。
CallSite的主要作用是将方法调用与实际被调用的方法的绑定推迟到运行时。其核心的方法 getTarget()可以获取到指定的MethodHandle

参数详解

  1. MethodHandles.Lookup caller - 具有访问权限的lookup
  2. String invokedName - 要实现方法的名称,目标方法的名称
  3. MethodType invokedType - 目标方法的类型
  4. MethodType samMethodType - 函数接口的类型
  5. MethodHandle implMethod - 实现目标方法的方法的 MethodHandle
  6. MethodType instantiatedMethodType - 返回的函数接口的类型

参数 MethodHandles.Lookup caller 比较好理解,查找目标方法的lookup,参数2,3,4描述了需要生成的函数映射(lambda方法),先说5,6两个参数。
5,6两个参数分别是需要映射的源方法的MethodHandle,以及对应的MethodType,上面的例子中方法句柄的描述是MethodHandle(LambdaMetafactoryBean,String)String,MethodType是(LambdaMetafactoryBean,String)String
含义是返回值类型是 String, 参数类型是 LambdaMetafactoryBean,String,对应描述的方法是 LambdaMetafactoryBean#say(String words),可见其第一个参数设置成了方法源类型(Class)。所以对应的 MethodType 才会是(LambdaMetafactoryBean,String)String
5,6参数既然已经描述了需要映射的原方法以及归属的Java类,那么2,3,4描述需要生成的函数映射只要对应上需要映射的原方法即可。按照(LambdaMetafactoryBean,String)String描述目标lambda应该是BiFunction,两个入参一个返回。
所以参数2的方法名应该是BiFunction对应的函数方法apply ,目标类型虽然是BiFunction,但参数3是需要MethodType的类型,所以填入MethodType.methodType(BiFunction.class),参数4描述的是函数接口类型,一个返回两个参数对应的是MethodType.methodType(Object.class, Object.class, Object.class)

以上就填满了LambdaMetafactory.metafactory方法了,生成是返回是CallSite类型。通过 CallSite.getTarget()获取到最终的函数映射的 MethodHandle

有个点需要额外在提一下,拿到最终需要的 MethodHandle之后我这边是直接调用的 invokeExact方法,且没有加任何参数,返回的也是BiFunction<LambdaMetafactoryBean, String, String>类型,并不是执行后的结果。
这里我主要是参考了fastjson2中的实现,fastjson2中大量使用了LambdaMetafactory.metafactory创建了许多映射且通过缓存或者直接赋给 static final类型保存了生成后的映射给后续调用(连这一步的操作都很多地方都放在static块中完成的),如此获得了极大的性能提升。
具体代码可以参见 fastjson2#ObjectWriterCreator

参考

fastjson2为什么这么快?

深入理解Java的Lambda原理

详解 Java 方法句柄 MethodHandle

LambdaMetafactory

MethodHandles.Lookup

FASTJSON2中的生成Getter函数映射代码

Method Handles in Java