反射真的很耗时吗?射10万次用时多久?

作者:DHL

无论是在面试过程中,还是看网络上各种技术文章,只要提到反射,不可避免都会提到一个问题,反射会影响性能吗?影响有多大?如果在写业务代码的时候,你用到了反射,都会被 review 人发出灵魂拷问,为什么要用反射,有没有其它的解决办法。

而网上的答案都是千篇一律,比如反射慢、反射过程中频繁的创建对象占用更多内存、频繁的触发 GC 等等。那么反射慢多少?反射会占用多少内存?创建 1 个对象或者创建 10 万个对象耗时多少?单次反射或者 10 万次反射耗时多少?在我们的脑海中没有一个直观的概念,而今天这篇文章将会告诉你。

这篇文章,设计了几个常用的场景,一起讨论一下反射是否真的很耗时?最后会以图表的形式展示。

测试工具及方案

在开始之前我们需要定义一个反射类 Person。

class 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 仓库 KtPractice:
https://github.com/hi-dhl/KtPractice

为什么使用 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 之间互相转化
// 获取 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.find { it.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.find { it.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.018 ms/op,速度得到了很大的提升,相比反射优化前速度提升了 4.7 倍,但是如果我们在将安全检查功能关掉呢。

constructor?.isAccessible = true

isAccessible 是用来判断是否需要进行安全检査,设置为 true 表示关掉安全检查,将会减少安全检査产生的耗时,五轮测试平均耗时 0.943 ms/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.find { it.name == "getName" }
        bh.consume(method?.call(person))
    }
}

五轮测试平均耗时 10.533 ms/op,是正常调用的 26 倍。如果我们将中间操作(获取 getName 代码)从循环中提取出来,结果会怎么样呢。

反射优化

@Benchmark
fun 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 倍,如果在将安全检查关掉呢。

method?.isAccessible = true

五轮测试平均耗时 0.687 ms/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.find { it.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.find { it.name == "age" }
    for (index in 0 until 100_000) {
        bh.consume(propertie?.call(person))
    }
}

将中间操作(获取属性的代码)从循环中提出来之后,五轮测试平均耗时 1.362 ms/op,速度得到了很大的提升,相比反射优化前速度提升了 8 倍,我们在将安全检查关掉,看一下结果。

propertie?.isAccessible = true

五轮测试平均耗时 1.202 ms/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?.find { it.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?.find { it.name == "getAddress" }
    for (index in 0 until 100_000) {
        bh.consume(compaion?.call(personInstance))
    }
}

将中间操作(获取 getAddress 代码)从循环中提出来,五轮测试平均耗时 0.840 ms/op,速度得到了很大的提升,相比反射优化前速度提升了 7 倍,现在我们在将安全检查关掉。

compaion?.isAccessible = true

五轮测试平均耗时 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 框架等等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容