背景
在使用Java
进行开发时,我们会不可避免的使用到大量的反射操作,比如Spring Boot
会在接收到HTTP
请求时,利用反射Controller
调用接口中的对应方法,或是Jackson
框架使用反射来解析json
中的数据给对应字段进行赋值,我们可以编写一个简单的JMH
测试来评估一下通过反射调用来创建对象的性能,与直接调用对象构造方法之间的差距:
1 | java复制代码 |
在Test
类中,具有一个简单的int
类型的变量,我们分别测试直接调用构造方法,赋值然后取值,以及使用Constructor
和Method
进行普通反射调用之间的性能对比,注意一定要将构造出来的对象使用Blackhole.consume()
方法给吃掉,这样JVM
才不会把没有使用到的变量给直接的优化掉,得出错误的测试结果,以上代码在笔者的机器上运行的结果如下:
1 | plaintext复制代码 |
可以看到,使用反射的性能比起直接调用来讲有非常大的差距,尤其是在这种极其简单的对象创建场景中,但是使用反射是很多情况下我们不得不采用的一个做法,那么我们有没有什么办法来尽可能优化一下反射调用的性能呢?
先让我们试一下MethodHandle
提供的方法调用模型,MethodHandle
是自JDK7
版本后开始推出的,用于替换旧反射调用的新方式,相比起原有的反射调用,提供了更多的交互方式,并且具备对Java
方法调用和Native
方法调用一致的模型,我们可以简单的创建一个用例进行测试:
1 | java复制代码 |
实测的结果则更加的不尽人意:
1 | plaintext复制代码 |
可以看到,使用MethodHandle
与使用普通反射之间的性能差距,就和普通反射与直接调用之间的差距一样大,事实上在JDK18
以后,根据# JEP 416: Reimplement Core Reflection with Method Handles 使用java.lang.reflect
和java.lang.invoke
的相关API
已经进行了相应的底层重构,转而使用MethodHandle
进行实现,很明显,在使用java.lang.reflect
和java.lang.invoke
中的方法时,与直接使用MethodHandle
相比,具备了更多的优化工作,根据官方的说法,在使用MethodHandle
时因将字段尽可能定义为static final
,这样JVM
可以将其进行常量折叠,从而实现巨大的性能提升,让我们修改一下以上的测试代码:
1 | java复制代码 |
得到了如下的数据:
1 | plaintext复制代码 |
突然之间,我们的反射调用和直接调用的性能已经完全一致了,那么这是不是意味着,我们想要的功能已经完全实现了呢?事实上并未如此,如果我们必须在static final
中指定需要使用到的反射字段,那么就相当于损失了绝大多数的灵活性,在实际操作中可行性并不高。
同样的,我们可以试一试,将直接使用java.lang.reflect
和java.lang.invoke
的函数所需的对象先构建并缓存在本地,再测试一下其对应的性能:
1 | java复制代码 |
与在测试MethodHandle
时我们将需要初始化的变量定义为static final
不同,此处我们直接将其定义为private
变量,在JMH
框架中提供的@Setup
函数中进行初始化,更贴合的模拟我们在运行时进行创建的行为,测试得到的结果如下:
1 | plaintext复制代码 |
可以看到,使用普通反射的方式,无论是每次都获取新的Constructor
或Method
对象进行创建,还是通过提前缓存的形式进行加载,性能表现是相似的,这也使得通用的反射调用方式在各类通用场景下都能够具备比较不错的表现。
鉴于我们之前的这些测试结果,如果想要进一步的提升反射的性能,只能考虑使用类生成的方式,在编译期创建出MethodHandle
的静态变量,让JVM
帮我们去自动内联,当然,类生成的方式一定可以拥有非常不错的性能,但是使用ByteBuddy
或Asm
框架进行类生成的代码相对而言过于繁琐,目前[# JEP 457: Class-File API (Preview)].(openjdk.org/jeps/457) 特性正处于preview
阶段,可以帮助我们更加简化的在JVM
中进行类生成,但是目前我们还无法对其进行使用。
解决方案
Lambda
表达式贯穿了我们日常的开发中的所有角落,且Lambda
表达式本身的性能不会差,否则JDK
内部绝对不会如此大量的使用它,Lambda
表达式的生成方式也并不复杂,其背后的核心方法是通过LambdaMetafactory.metafactory()
方法生成对应的方法调用,我们可是实现以下的代码来完成对应构造函数,getter
方法和setter
方法向Lambda
函数的转换:
1 | java复制代码 |
测试分为两个步骤,一个是测试Lambda
表达式的生成性能,一个是测试Lambda
表达式的运行性能,这两个指标对我们来说都非常的重要,得到的结果如下:
1 | plaintext复制代码 |
1 | plaintext复制代码Benchmark (size) Mode Cnt Score Error Units |
可以看到,通过模拟Lambda
表达式生成的方式,调用构造函数以及get
和set
方法的性能,与直接调用是几乎完全一致的,这也就达成了我们想要的效果,但是Lambda
生成的性能非常不容乐观,与直接使用箭头函数进行生成的性能有着天壤之别,好在如果Lambda
表达式没有捕获任何的外部变量,比如我们在示例中调用的get
和set
方法,那么生成的方法是可以被缓存起来重复使用的,如果使用的基数本身比较大,在多次调用的开销权衡中,初始化的开销就可以被忽略不计。
小结
本文介绍了一种在Java
中的新的反射调用方式,即使用类似于Lambda
表达式的生成的方式进行反射,可以将一些简单的方法,例如get
和set
方法,直接转化为相应的Lambda
表达式来调用,虽然可以做到和直接调用一致的性能,但是该方法的生成开销比较大,需要在频繁调用的场景中进行缓存,才能起到比较好的效果。
本文转载自: 掘金