揭秘反射真的很耗时吗,反射 10 万次,耗时多久
hi
这是 dhl
的第 66 篇原创文章个人微信: hi-dhl
hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。
无论是在面试过程中,还是看网络上各种技术文章,只要提到反射,不可避免都会提到一个问题,反射会影响性能吗?影响有多大?如果在写业务代码的时候,你用到了反射,都会被 review 人发出灵魂拷问,为什么要用反射,有没有其它的解决办法。
而网上的答案都是千篇一律,比如反射慢、反射过程中频繁的创建对象占用更多内存、频繁的触发 GC 等等。那么反射慢多少?反射会占用多少内存?创建 1 个对象或者创建 10 万个对象耗时多少?单次反射或者 10 万次反射耗时多少?在我们的脑海中没有一个直观的概念,而今天这篇文章将会告诉你。
这篇文章,设计了几个常用的场景,一起讨论一下反射是否真的很耗时?最后会以图表的形式展示。
测试工具及方案
在开始之前我们需要定义一个反射类 Person
。
var age = 10
fun getName(): String {
return "I am DHL"
}
companion object {
fun getAddress(): String = "BJ"
}
}
针对上面的测试类,设计了以下几个常用的场景,验证反射前后的耗时。
创建对象
方法调用
属性调用
伴生对象
测试工具及代码:
JMH (Java Microbenchmark Harness),这是 Oracle 开发的一个基准测试工具,他们比任何人都了解 JIT 以及 JVM 的优化对测试过程中的影响,所以使用这个工具可以尽可能的保证结果的可靠性。
基准测试是测试应用性能的一种方法,在特定条件下对某一对象的性能指标进行测试
本文的测试代码已经上传到 github 仓库 KtPractice 欢迎前往查看。Github 地址贴在了评论区
为什么使用 JMH
因为 JVM 会对代码做各种优化,如果只是在代码前后打印时间戳,这样计算的结果是不置信的,因为忽略了 JVM 在执行过程中,对代码进行优化产生的影响。而 JMH 会尽可能的减少这些优化对最终结果的影响。
测试方案
在单进程、单线程中,针对以上四个场景,每个场景测试五轮,每轮循环 10 万次,计算它们的平均值
在执行之前,需要对代码进行预热,预热不会作为最终结果,预热的目的是为了构造一个相对稳定的环境,保证结果的可靠性。因为 JVM 会对执行频繁的代码,尝试编译为机器码,从而提高执行速度。而预热不仅包含编译为机器码,还包含 JVM 各种优化算法,尽量减少 JVM 的优化,构造一个相对稳定的环境,降低对结果造成的影响。
JMH 提供 Blackhole,通过 Blackhole 的 consume 来避免 JIT 带来的优化
Kotlin 和 Java 的反射机制
本文测试代码全部使用 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
之间互相转化
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
Person().javaClass.getDeclaredField("age")
// Koltin 的获取方式
Person::class.declaredMemberProperties.find { it.name == "age" }
在 Kotlin 中 函数 、属性 以及 构造函数 的超类型都是
KCallable
,对应的子类型是KFunction
(函数、构造方法等等) 和KProperty / KMutableProperty
(属性),而 Kotlin 中的KCallable
对应 Java 的AccessibleObject
, 其子类型分别是Method
、Field
、Constructor
Person().javaClass.getConstructor().newInstance() // 构造方法
Person().javaClass.getDeclaredMethod("getName") // 成员方法
// Kotlin
Person::class.primaryConstructor?.call() // 构造方法
Person::class.declaredFunctions.find { it.name == "getName" } // 成员方法
无论是使用 Java 还是 Kotlin 最终测试出来的结论都是一样的,了解完基本反射语法之后,我们分别测试上述四种场景反射前后的耗时。
创建对象
正常创建对象
@Benchmarkfun createInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person())
}
}
五轮测试平均耗时 0.578 ms/op
。需要重点注意,这里使用了 JMH 提供 Blackhole
,通过 Blackhole
的 consume()
方法来避免 JIT 带来的优化, 让结果更加接近真实。
在对象创建过程中,会先检查类是否已经加载,如果类已经加载了,会直接为对象分配空间,其中最耗时的阶段其实是类的加载过程(加载->验证->准备->解析->初始化)。
通过反射创建对象
@Benchmarkfun createReflectInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person::class.primaryConstructor?.call())
}
}
五轮测试平均耗时 4.710 ms/op
,是正常创建对象的 9.4 倍
,这个结果是很惊人,如果将中间操作(获取构造方法)从循环中提取出来,那么结果会怎么样呢。
反射优化
@Benchmarkfun createReflectInstanceAccessibleTrue(bh: Blackhole) {
val constructor = Person::class.primaryConstructor
for (index in 0 until 100_000) {
bh.consume(constructor?.call())
}
}
正如你所见,我将中间操作(获取构造方法)从循环中提取出来,五轮测试平均耗时 1.018 ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 4.7
倍,但是如果我们在将安全检查功能关掉呢。
isAccessible
是用来判断是否需要进行安全检査,设置为 true
表示关掉安全检查,将会减少安全检査产生的耗时,五轮测试平均耗时 0.943 ms/op
,反射速度进一步提升了。
几轮测试最后的结果如下图示。
方法调用
正常调用
@Benchmarkfun callMethod(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh.consume(person.getName())
}
}
五轮测试平均耗时 0.422 ms/op
。
反射调用
@Benchmarkfun callReflectMethod(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
val method = Person::class.declaredFunctions.find { it.name == "getName" }
bh.consume(method?.call(person))
}
}
五轮测试平均耗时 10.533 ms/op
,是正常调用的 26 倍
。如果我们将中间操作(获取 getName
代码)从循环中提取出来,结果会怎么样呢。
反射优化
@Benchmarkfun callReflectMethodAccessiblFalse(bh: Blackhole) {
val person = Person()
val method = Person::class.declaredFunctions.find { it.name == "getName" }
for (index in 0 until 100_000) {
bh.consume(method?.call(person))
}
}
将中间操作(获取 getName
代码)从循环中提取出来了,五轮测试平均耗时 0.844 ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 13
倍,如果在将安全检查关掉呢。
五轮测试平均耗时 0.687 ms/op
,反射速度进一步提升了。
几轮测试最后的结果如下图示。
属性调用
正常调用
@Benchmarkfun callPropertie(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh.consume(person.age)
}
}
五轮测试平均耗时 0.241 ms/op
。
反射调用
@Benchmarkfun callReflectPropertie(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
val propertie = Person::class.declaredMemberProperties.find { it.name == "age" }
bh.consume(propertie?.call(person))
}
}
五轮测试平均耗时 12.432 ms/op
,是正常调用的 62 倍
,然后我们将中间操作(获取属性的代码)从循环中提出来。
反射优化
@Benchmarkfun callReflectPropertieAccessibleFalse(bh: Blackhole) {
val person = Person::class.createInstance()
val propertie = Person::class.declaredMemberProperties.find { it.name == "age" }
for (index in 0 until 100_000) {
bh.consume(propertie?.call(person))
}
}
将中间操作(获取属性的代码)从循环中提出来之后,五轮测试平均耗时 1.362 ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 8
倍,我们在将安全检查关掉,看一下结果。
五轮测试平均耗时 1.202 ms/op
,反射速度进一步提升了。
几轮测试最后的结果如下图示。
伴生对象
正常调用
@Benchmarkfun callCompaion(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person.getAddress())
}
}
五轮测试平均耗时 0.470 ms/op
。
反射调用
@Benchmarkfun 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?.find { it.name == "getAddress" }
bh.consume(compaion?.call(personInstance))
}
}
五轮测试平均耗时 5.661 ms/op
,是正常调用的 11 倍
,然后我们在看一下将中间操作(获取 getAddress
代码)从循环中提出来的结果。
反射优化
@Benchmarkfun callReflectCompaionAccessibleFalse(bh: Blackhole) {
val classes = Person::class
val personInstance = classes.companionObjectInstance
val personObject = classes.companionObject
val compaion = personObject?.declaredFunctions?.find { it.name == "getAddress" }
for (index in 0 until 100_000) {
bh.consume(compaion?.call(personInstance))
}
}
将中间操作(获取 getAddress
代码)从循环中提出来,五轮测试平均耗时 0.840 ms/op
,速度得到了很大的提升,相比反射优化前速度提升了 7
倍,现在我们在将安全检查关掉。
五轮测试平均耗时 0.702 ms/op
,反射速度进一步提升了。
几轮测试最后的结果如下图所示。
总结
我们对比了四种常用的场景: 创建对象、方法调用、属性调用、伴生对象。分别测试了反射前后的耗时,最后汇总一下五轮 10 万次测试平均值(表格可滑动)。
正常调用 | 反射 | 反射优化后 | 反射优化后关掉安全检查 | |
---|---|---|---|---|
创建对象 | 0.578 ms/op | 4.710 ms/op | 1.018 ms/op | 0.943 ms/op |
方法调用 | 0.422 ms/op | 10.533 ms/op | 0.844 ms/op | 0.687 ms/op |
属性调用 | 0.241 ms/op | 12.432 ms/op | 1.362 ms/op | 1.202 ms/op |
伴生对象 | 0.470 ms/op | 5.661 ms/op | 0.840 ms/op | 0.702 ms/op |
每个场景反射前后的耗时如下图所示。
在我们的印象中,反射就是恶魔,影响会非常大,但是从上面的表格看来,反射确实会有一定的影响,但是如果我们合理使用反射,优化后的反射结果并没有想象的那么大,这里有几个建议。
在频繁的使用反射的场景中,将反射中间操作提取出来缓存好,下次在使用反射直接从缓存中取即可
关掉安全检查,可以进一步提升性能
最后我们在看一下单次创建对象和单次反射创建对象的耗时,如下图所示。
Score
表示结果,Error
表示误差范围,在考虑误差的情况下,它们的耗时差距在 微妙别以内。
当然根据设备的不同(高端机、低端机),还有系统、复杂的类等等因素,反射所产生的影响也是不同的。反射在实际项目中应用的非常的广泛,很多设计和开发都和反射有关,比如通过反射去调用字节码文件、调用系统隐藏 Api、动态代理的设计模式,Android 逆向、著名的 Spring 框架、各类 Hook 框架等等。
推荐阅读:
揭秘 Kotlin 1.6.20 重磅功能 Context Receivers
Stack Overflow 上最热门的 10 个 Kotlin 问题?
全文到这里就结束了,如果对你有帮助,欢迎 在看、点赞、收藏、分享 给身边的朋友。
公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。
👇🏻 真诚推荐你关注我👇🏻
因微信公众号更改了推送机制
可能无法及时看到最新文章
将公众号设为 星标
或常为文章点 在看
即可及时收到最新文章
欢迎前往 博客 查看更多 Kotlin、Jetpack 、动画算法图解、系统源码分析等等文章。以及开源项目、LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解。
https://www.hi-dhl.com