昨天看了 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 新增的类 LambdaMetafactory的metafactory方法。其方法包含六个参数
- MethodHandles.Lookup caller
- String invokedName
- MethodType invokedType
- MethodType samMethodType
- MethodHandle implMethod
- MethodType instantiatedMethodType
先看下使用的例子,在详细讲下这六个参数。
1 | public class LambdaMetafactoryTest { |
输出
1 | hi lucy ,nice to meet you. |
讲参数之前,需要先看下MethodType ,MethodHandle 这几个参数类型。
参数类型
MethodType
MethodType 是java1.7 新增的用于描述方法类型的类。
它可以描述方法的返回类型与参数类型。所有的类型都是用其Class表示,使用Void.class代表没有返回类型。MethodType的实例只能由其工厂方法创建,工厂方法可能是有缓存的,但并不保证。创建的实例都是不可变的(immutable)。
常用方法包含:
methodType()方法:用于创建一个新的MethodType对象,参数为方法的返回类型和参数类型。changeReturnType()方法:用于改变MethodType对象的返回类型。changeParameterType()方法:用于改变MethodType对象的参数类型。insertParameterTypes()方法:用于在MethodType对象中插入一个或多个新的参数类型。erase()方法:用于返回一个擦除了泛型信息的MethodType对象。
最重要的还是methodType()方法,可以通过它创建一个MethodType的实例。比如示例中的方法签名:
1 | public String say(String 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步
- 创建lookup
- 创建method type
- 找到method handle
- 调用方method handle
例子
1 | // 测试类 |
更详细的说明参见 Method Handles in Java
CallSite
CallSite 代表一个方法调用点,是Java1.7引入的的一个新特性,为了支持动态语言的实现而设计的。CallSite的主要作用是将方法调用与实际被调用的方法的绑定推迟到运行时。其核心的方法 getTarget()可以获取到指定的MethodHandle。
参数详解
- MethodHandles.Lookup caller - 具有访问权限的lookup
- String invokedName - 要实现方法的名称,目标方法的名称
- MethodType invokedType - 目标方法的类型
- MethodType samMethodType - 函数接口的类型
- MethodHandle implMethod - 实现目标方法的方法的 MethodHandle
- 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