要须心地收汗马,孔孟行世目杲杲。这篇文章主要讲述揭秘反射真的很耗时吗,射 10 万次用时多久相关的知识,希望能为你提供帮助。
全文分为 视频版 和 文字版,
- 文字版: 侧重于细节上的知识点更多、更加详细
- 视频版: 通过动画展示讲解,更加的清楚、直观
无论是在面试过程中,还是看网络上各种技术文章,只要提到反射,不可避免都会提到一个问题,反射会影响性能吗?影响有多大?如果在写业务代码的时候,你用到了反射,都会被 review 人发出灵魂拷问,为什么要用反射,有没有其它的解决办法。
而网上的答案都是千篇一律,比如反射慢、反射过程中频繁的创建对象占用更多内存、频繁的触发 GC 等等。那么反射慢多少?反射会占用多少内存?创建 1 个对象或者创建 10 万个对象耗时多少?单次反射或者 10 万次反射耗时多少?在我们的脑海中没有一个直观的概念,而今天这篇文章将会告诉你。
这篇文章,设计了几个常用的场景,一起讨论一下反射是否真的很耗时?最后会以图表的形式展示。
测试工具及方案
在开始之前我们需要定义一个反射类
Person
。class Person
var age = 10fun getName(): String
return "I am DHL"companion object
fun getAddress(): String = "BJ"
针对上面的测试类,设计了以下几个常用的场景,验证反射前后的耗时。
- 创建对象
- 方法调用
- 属性调用
- 伴生对象
JMH (java Microbenchmark Harness),这是 Oracle 开发的一个基准测试工具,他们比任何人都了解 JIT 以及 JVM 的优化对测试过程中的影响,所以使用这个工具可以尽可能的保证结果的可靠性。
本文的测试代码已经上传到 github 仓库 KtPractice 欢迎前往查看。
【揭秘反射真的很耗时吗,射 10 万次用时多久】github 仓库 KtPractice: https://github.com/hi-dhl/KtPractice
为什么使用 JMH
因为 JVM 会对代码做各种优化,如果只是在代码前后打印时间戳,这样计算的结果是不置信的,因为忽略了 JVM 在执行过程中,对代码进行优化产生的影响。而 JMH 会尽可能的减少这些优化对最终结果的影响。
测试方案
- 在单进程、单线程中,针对以上四个场景,每个场景测试五轮,每轮循环 10 万次,计算它们的平均值
- 在执行之前,需要对代码进行预热,预热不会作为最终结果,预热的目的是为了构造一个相对稳定的环境,保证结果的可靠性。因为 JVM 会对执行频繁的代码,尝试编译为机器码,从而提高执行速度。而预热不仅包含编译为机器码,还包含 JVM 各种优化算法,尽量减少 JVM 的优化,构造一个相对稳定的环境,降低对结果造成的影响。
- JMH 提供 Blackhole,通过 Blackhole 的 consume 来避免 JIT 带来的优化
本文测试代码全部使用 Kotlin,Koltin 是完美兼容 Java 的,所以同样也可以使用 Java 的反射机制,但是 Kotlin 自己也封装了一套反射机制,并不是用来取代 Java 的,是 Java 的增强版,因为 Kotlin 有自己的语法特点比如扩展方法 、伴生对象 、可空类型的检查等等,如果想使用 Kotlin 反射机制,需要引入以下库。
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
在开始分析,我们需要对比 Java 了解一下 Kotlin 反射基本语法。
- kotlin 的
KClass
对应 Java 的Class
,我们可以通过以下方式完成KClass
和Class
之间互相转化
// 获取 Class
Person().javaClass
Person()::class.java
Person::class.java
Class.forName("com.hi-dhl.demo.Person")// 获取 KClass
Person().javaClass.kotlin
Person::class
Class.forName("com.hi-dhl.demo.Person").kotlin
- kotlin 的
KProperty
对应 Java 的Field
,Java 的Field
有getter/setter
方法,但是在 Kotlin 中没有Field
,分为了KProperty
和KMutableProperty
,当变量用val
声明的时候,即属性为KProperty
,如果变量用var
声明的时候,即属性为KMutableProperty
// Java 的获取方式
Person().javaClass.getDeclaredField("age")// Koltin 的获取方式
Person::class.declaredMemberProperties.findit.name == "age"
- 在 Kotlin 中 函数 、属性 以及 构造函数 的超类型都是
KCallable
,对应的子类型是KFunction
(函数、构造方法等等) 和KProperty / KMutableProperty
(属性),而 Kotlin 中的KCallable
对应 Java 的AccessibleObject
, 其子类型分别是Method
、Field
、Constructor
// Java
Person().javaClass.getConstructor().newInstance() // 构造方法
Person().javaClass.getDeclaredMethod("getName") // 成员方法// Kotlin
Person::class.primaryConstructor?.call() // 构造方法
Person::class.declaredFunctions.findit.name == "getName"// 成员方法
无论是使用 Java 还是 Kotlin 最终测试出来的结论都是一样的,了解完基本反射语法之后,我们分别测试上述四种场景反射前后的耗时。
创建对象
正常创建对象
@Benchmark
fun createInstance(bh: Blackhole)
for (index in 0 until 100_000)
bh.consume(Person())
五轮测试平均耗时
0.578 ms/op
。需要重点注意,这里使用了 JMH 提供 Blackhole
,通过 Blackhole
的 consume()
方法来避免 JIT 带来的优化, 让结果更加接近真实。在对象创建过程中,会先检查类是否已经加载,如果类已经加载了,会直接为对象分配空间,其中最耗时的阶段其实是类的加载过程(加载-> 验证-> 准备-> 解析-> 初始化)。
通过反射创建对象
@Benchmark
fun createReflectInstance(bh: Blackhole)
for (index in 0 until 100_000)
bh.consume(Person::class.primaryConstructor?.call())
五轮测试平均耗时
4.710 ms/op
,是正常创建对象的 9.4 倍
,这个结果是很惊人,如果将中间操作(获取构造方法)从循环中提取出来,那么结果会怎么样呢。反射优化
@Benchmark
fun createReflectInstanceAccessibleTrue(bh: Blackhole)
val constructor = Person::class.primaryConstructor
for (index in 0 until 100_000)
bh.consume(constructor?.call())
正如你所见,我将中间操作(获取构造方法)从循环中提取出来,五轮测试平均耗时
1.018ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 4.7
倍,但是如果我们在将安全检查功能关掉呢。constructor?.isAccessible = true
isAccessible
是用来判断是否需要进行安全检査,设置为 true
表示关掉安全检查,将会减少安全检査产生的耗时,五轮测试平均耗时 0.943ms/op
,反射速度进一步提升了。几轮测试最后的结果如下图示。
文章图片
方法调用
正常调用
@Benchmark
fun callMethod(bh: Blackhole)
val person = Person()
for (index in 0 until 100_000)
bh.consume(person.getName())
五轮测试平均耗时
0.422 ms/op
。反射调用
@Benchmark
fun callReflectMethod(bh: Blackhole)
val person = Person()
for (index in 0 until 100_000)
val method = Person::class.declaredFunctions.findit.name == "getName"
bh.consume(method?.call(person))
五轮测试平均耗时
10.533ms/op
,是正常调用的 26 倍
。如果我们将中间操作(获取 getName
代码)从循环中提取出来,结果会怎么样呢。反射优化
@Benchmark
fun callReflectMethodAccessiblFalse(bh: Blackhole)
val person = Person()
val method = Person::class.declaredFunctions.findit.name == "getName"
for (index in 0 until 100_000)
bh.consume(method?.call(person))
将中间操作(获取
getName
代码)从循环中提取出来了,五轮测试平均耗时 0.844ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 13
倍,如果在将安全检查关掉呢。method?.isAccessible = true
五轮测试平均耗时
0.687ms/op
,反射速度进一步提升了。几轮测试最后的结果如下图示。
文章图片
属性调用
正常调用
@Benchmark
fun callPropertie(bh: Blackhole)
val person = Person()
for (index in 0 until 100_000)
bh.consume(person.age)
五轮测试平均耗时
0.241 ms/op
。反射调用
@Benchmark
fun callReflectPropertie(bh: Blackhole)
val person = Person()
for (index in 0 until 100_000)
val propertie = Person::class.declaredMemberProperties.findit.name == "age"
bh.consume(propertie?.call(person))
五轮测试平均耗时
12.432 ms/op
,是正常调用的 62 倍
,然后我们将中间操作(获取属性的代码)从循环中提出来。反射优化
@Benchmark
fun callReflectPropertieAccessibleFalse(bh: Blackhole)
val person = Person::class.createInstance()
val propertie = Person::class.declaredMemberProperties.findit.name == "age"
for (index in 0 until 100_000)
bh.consume(propertie?.call(person))
将中间操作(获取属性的代码)从循环中提出来之后,五轮测试平均耗时
1.362ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 8
倍,我们在将安全检查关掉,看一下结果。propertie?.isAccessible = true
五轮测试平均耗时
1.202ms/op
,反射速度进一步提升了。几轮测试最后的结果如下图示。
文章图片
伴生对象
正常调用
@Benchmark
fun callCompaion(bh: Blackhole)
for (index in 0 until 100_000)
bh.consume(Person.getAddress())
五轮测试平均耗时
0.470 ms/op
。反射调用
@Benchmark
fun createReflectCompaion(bh: Blackhole)
val classes = Person::class
val personInstance = classes.companionObjectInstance
val personObject = classes.companionObject
for (index in 0 until 100_000)
val compaion = personObject?.declaredFunctions?.findit.name == "getAddress"
bh.consume(compaion?.call(personInstance))
五轮测试平均耗时
5.661 ms/op
,是正常调用的 11 倍
,然后我们在看一下将中间操作(获取 getAddress
代码)从循环中提出来的结果。反射优化
@Benchmark
fun callReflectCompaionAccessibleFalse(bh: Blackhole)
val classes = Person::class
val personInstance = classes.companionObjectInstance
val personObject = classes.companionObject
val compaion = personObject?.declaredFunctions?.findit.name == "getAddress"
for (index in 0 until 100_000)
bh.consume(compaion?.call(personInstance))
将中间操作(获取
getAddress
代码)从循环中提出来,五轮测试平均耗时 0.840 ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 7
倍,现在我们在将安全检查关掉。compaion?.isAccessible = true
五轮测试平均耗时
0.702ms/op
,反射速度进一步提升了。几轮测试最后的结果如下图所示。
文章图片
总结
我们对比了四种常用的场景: 创建对象、方法调用、属性调用、伴生对象。分别测试了反射前后的耗时,最后汇总一下五轮 10 万次测试平均值。
正常调用 | 反射 | 反射优化后 | 反射优化后关掉安全检查 | |
---|---|---|---|---|
创建对象 | 0.578 ms/op | 4.710 ms/op | 1.018ms/op | 0.943ms/op |
方法调用 | 0.422 ms/op | 10.533ms/op | 0.844ms/op | 0.687ms/op |
属性调用 | 0.241 ms/op | 12.432 ms/op | 1.362ms/op | 1.202ms/op |
伴生对象 | 0.470 ms/op | 5.661 ms/op | 0.840ms/op | 0.702ms/op |
文章图片
在我们的印象中,反射就是恶魔,影响会非常大,但是从上面的表格看来,反射确实会有一定的影响,但是如果我们合理使用反射,优化后的反射结果并没有想象的那么大,这里有几个建议。
- 在频繁的使用反射的场景中,将反射中间操作提取出来缓存好,下次在使用反射直接从缓存中取即可
- 关掉安全检查,可以进一步提升性能
文章图片
Score
表示结果,Error
表示误差范围,在考虑误差的情况下,它们的耗时差距在 微妙别以内。当然根据设备的不同(高端机、低端机),还有系统、复杂的类等等因素,反射所产生的影响也是不同的。反射在实际项目中应用的非常的广泛,很多设计和开发都和反射有关,比如通过反射去调用字节码文件、调用系统隐藏 Api、动态代理的设计模式,android 逆向、著名的 Spring 框架、各类 Hook 框架等等。
文章中的代码已经上传到 github 仓库 KtPractice: https://github.com/hi-dhl/KtPractice
< br/>
全文到这里就结束了,感谢你的阅读,如果有帮助,欢迎
在看
、 点赞
、 收藏
、 分享
给身边的朋友。真诚推荐你关注我,公众号:ByteCode ,持续分享硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。
< br/>
推荐阅读
- Python学习(自动化测试selenium的指定截图文件名(动态时间))
- 阿里云镜像迁移到Harbor详细的操作步骤
- 如何使用Python实现图像融合及加法运算()
- 大话数据结构溢彩加强版绪论篇
- Approximate Graph Propagation
- 云视界(一个分布式的应用软件)
- PIMFOpenHarmony啃论文俱乐部(拼音输入法_从触摸事件到汉字)
- Harmony OSArkUIets开发 图形与动画绘制
- G020-OP-INS-RHEL-02 RedHat OpenStack 发放云主机(命令行)