反射太慢?那是你不会用LambdaMetafactory!
【摘要】 本文将深入浅出的介绍LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势
反射太慢?那是你不会用LambdaMetafactory!
在Java的世界里,反射一直是开发者们既爱又恨的功能
一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已
在之前的文章中,我们介绍使用Spring工具类ReflectionUtils通过缓存、避免创建临时对象的方式来优化反射的性能
但工具类在调用方法时依旧会使用反射的invoke,在高频调用的场景性能还是会与直接调用相差一大截
随着Java 8的到来,LambdaMetafactory为我们打开了新的大门,它不仅能够使用类似反射的灵活性,在高频调用场景下还能与直接调用的性能相差不大
本文将深入浅出的介绍LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势(导图如下)
使用LambdaMetafactory
LambdaMetafactory是Java 8引入的一个类,位于java.lang.invoke
包下
它的主要任务是在运行时创建lambda表达式的实现
LambdaMetafactory通过MethodHandle和CallSite机制工作,它能够在运行时通过ASM字节码库生成lambda内部类来调用目标方法,从而兼得反射的灵活性与直接调用的性能优势
MethodHandle是方法句柄,通过它可以灵活调用目标方法;CallSite用于动态管理存储方法句柄MethodHandle,它们是实现动态调用的关键
LambdaMetafactory的核心在于其LambdaMetafactory.metafactory
方法
该方法根据目标方法、接口方法等数据来定义目标lambda的行为,并返回一个CallSite对象
后续通过获取CallSite的目标方法句柄进行调用
使用方法通常分为以下几个步骤:
- 使用Lookup查找要调用的目标方法句柄MethodHandle
- 根据要生成的lambda方法名、调用接口、目标方法类型(返回、入参类型)、句柄等信息创建CallSite
- 根据CallSite的MethodHandle获取生成的函数接口实现类再调用接口方法
定义的函数式接口
@FunctionalInterface
interface Greeter {
void greet(String name);
}
使用LambdaMetafactory
public class LambdaMetafactoryExample {
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
public static void main(String[] args) throws Throwable {
// 获取 Lookup 对象
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 编写方法类型 返回、入参类型
MethodType methodType = MethodType.methodType(void.class, String.class);
// 查找目标方法句柄
MethodHandle targetMethod = lookup.findStatic(LambdaMetafactoryExample.class, "sayHello", methodType);
// 准备元数据并创建 CallSite
CallSite callSite = LambdaMetafactory.metafactory(
lookup,
"greet", // 要生成的lambda方法名
MethodType.methodType(Greeter.class), // 调用点签名
methodType, // 目标方法类型(注意这里应该直接是 methodType)
targetMethod, // 目标方法句柄
methodType // 目标方法类型
);
// 获取并调用
MethodHandle factory = callSite.getTarget();
Greeter greeter = (Greeter) factory.invokeWithArguments();
// 调用接口方法
greeter.greet("World");
}
}
总的来说,LambdaMetafactory就是通过lambda生成接口方法,从而实现灵活调用目标方法
性能对比
反射性能上带来的劣势主要有以下几个原因:
- 调用时需要安全检查、类型转换,查找到方法、字段后会创建临时对象
- 方法、字段的缓存不是强引用,gc后会被清空
- 动态运行,高频访问无法使用JIT优化
JVM通过解释、编译混合运行,对于高频访问的代码会使用JIT将字节码转化为机器码缓存在方法区,无需再进行解释执行,在高频访问的场景下反射无法使用该优化
而LambdaMetafactory相比反射具有显著的性能优势,主要原因在于:
- 减少开销:减少每次调用时的安全检查和类型转换等操作,使用Lookup进行查找
- 避免重复计算:一旦lambda被初始化,后续调用几乎无额外开销
- 即时编译:生成的代码可以被JVM即时编译器(JIT)优化
这些因素使得LambdaMetafactory在循环、频繁调用的场景中尤为出色
测试代码如下,也可以直接看后面的结果表格:
public class LambdaVsReflectionBenchmark {
// 目标方法
public static void sayHello(String name) {
name = "Hello," + name;
// System.out.println("Hello, " + name);
}
// 使用 LambdaMetafactory 创建 lambda 表达式
private static Greeter createLambda() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle targetMethod = lookup.findStatic(LambdaVsReflectionBenchmark.class, "sayHello", methodType);
CallSite callSite = LambdaMetafactory.metafactory(
lookup,
"greet",
MethodType.methodType(Greeter.class),
methodType.changeReturnType(void.class),
targetMethod,
methodType
);
MethodHandle factory = callSite.getTarget();
return (Greeter) factory.invokeExact();
}
// 使用反射获取目标方法
private static Method getReflectiveMethod() throws Exception {
return LambdaVsReflectionBenchmark.class.getMethod("sayHello", String.class);
}
// 测试调用次数
// private static final long ITERATIONS = 1_000L;
private static final long ITERATIONS = 1_000_000_000L;
/**
* 调用次数:1000
* 直接调用耗时: 0.13 ms
* LambdaMetafactory 调用耗时: 0.17 ms
* 反射调用耗时: 1.74 ms
* <p>
* 调用次数:1000000000
* 直接调用耗时: 4501.54 ms
* LambdaMetafactory 调用耗时: 4640.59 ms
* 反射调用耗时: 6142.39 ms
*
* @param args
* @throws Throwable
*/
public static void main(String[] args) throws Throwable {
System.out.println("调用次数:" + ITERATIONS);
//直接调用
long start = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
sayHello("World");
}
long end = System.nanoTime();
System.out.printf("直接调用耗时: %.2f ms%n", (end - start) / 1e6);
// 准备 Lambda
Greeter lambdaGreeter = createLambda();
// 测试 Lambda 调用
long lambdaStart = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
lambdaGreeter.greet("World");
}
long lambdaEnd = System.nanoTime();
System.out.printf("LambdaMetafactory 调用耗时: %.2f ms%n", (lambdaEnd - lambdaStart) / 1e6);
// 准备反射方法
Method reflectiveMethod = getReflectiveMethod();
// 测试反射调用
long reflectionStart = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
reflectiveMethod.invoke(null, "World");
}
long reflectionEnd = System.nanoTime();
System.out.printf("反射调用耗时: %.2f ms%n", (reflectionEnd - reflectionStart) / 1e6);
}
直接调用 | LambdaMetafactory | 反射 | |
---|---|---|---|
循环1000次 | 0.13 ms | 0.17 ms | 1.74 ms |
循环1000000000次 | 4501.54 ms | 4640.59 ms | 6142.39 ms |
根据表格可以看出在高频调用的场景下,LambdaMetafactory与直接调用性能几乎相同,而反射性能几乎慢了将近四分之一
LambdaMetafactory工作原理
我们从核心方法 LambdaMetafactory.metafactory
生成CallSite作为入口进行分析其实现原理
该方法通过一系列的元数据,使用工厂来构建CallSite
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();
//生产CallSite
return mf.buildCallSite();
}
InnerClassLambdaMetafactory
工厂主要为lambda调用点创建内部类、以及封装构建CallSite
buildCallSite
构建CallSite时:先通过 spinInnerClass
方法创建内部类,再根据元数据在内部类中找到MethodHandle封装为CallSite
创建内部类是通过ASM字节码库的ClassWriter,将元数据写入后转换为流,最后通过UNSAFE类的defineAnonymousClass
生成类(逻辑代码如下)
private Class<?> spinInnerClass() throws LambdaConversionException {
//元数据写入ClassWriter
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
lambdaClassName, null,
JAVA_LANG_OBJECT, interfaces);
//转为字节流
final byte[] classBytes = cw.toByteArray();
//UNSAFE生成
return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
}
总的来说,LambdaMetafactory实际上是使用指定的元数据,通过ASM字节码库动态生成内部类,通过调用内部类接口方法来间接实现调用目标方法
这样就即实现反射调用的灵活,又能享受直接调用的性能,只是初次生成类存在一定的开销
(实际上这也是使用Lambda语法糖时会隐式帮助我们做的事情)
LambdaMetafactory虽然能够带来性能优势,但也存在一定的劣势,比如:依赖接口、使用更复杂…
根据不同的应用场景可以从反射、反射工具类、LambdaMetafactory中选择最适合的解决方案
总结
在高频使用反射的场景下,常常会有创建临时对象、软引用缓存被gc清空、无法使用JIT优化等问题而导致性能受到影响的情况
LambdaMetafactory带来了更加优雅的动态调用方式,虽然会有部分生成内部类的开销,但它解决了长期以来困扰开发者的反射性能问题
LambdaMetafactory使用元数据通过ASM字节码库、Unsafe类动态生成匿名内部类,再封装为Methodhandler、CallSite进行使用
(同时它也是Lambda语法糖的隐式实现,对于开发者透明)
对于不同的应用场景可以选择反射、Spring ReflectionUtils、LambdaMetafactory等多种方案进行解决问题
最后(一键三连求求拉~)
😊我是菜菜,热爱技术交流、分享与写作,喜欢图文并茂、通俗易懂的输出知识
📚在我的博客中,你可以找到Java技术栈的各个专栏:Java并发编程与JVM原理、Spring和MyBatis等常用框架及Tomcat服务器的源码解析,以及MySQL、Redis数据库的进阶知识,同时还提供关于消息中间件和Netty等主题的系列文章,都以通俗易懂的方式探讨这些复杂的技术点
🏆除此之外,我还是掘金优秀创作者、腾讯云年度影响力作者、华为云年度十佳博主…
👫我对技术交流、知识分享以及写作充满热情,如果你愿意,欢迎加我一起交流(vx:CaiCaiJava666),也可以持续关注我的公众号:菜菜的后端私房菜,我会分享更多技术干货,期待与更多志同道合的朋友携手并进,一同在这条充满挑战与惊喜的技术之旅中不断前行
🤝如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
📖本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔
📝本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以star持续关注喔~
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)