作者:DHL
無(wú)論是在面試過(guò)程中,還是看網(wǎng)絡(luò)上各種技術(shù)文章鳞骤,只要提到反射窒百,不可避免都會(huì)提到一個(gè)問(wèn)題,反射會(huì)影響性能嗎豫尽?影響有多大篙梢?如果在寫業(yè)務(wù)代碼的時(shí)候,你用到了反射美旧,都會(huì)被 review 人發(fā)出靈魂拷問(wèn)渤滞,為什么要用反射,有沒有其它的解決辦法榴嗅。
而網(wǎng)上的答案都是千篇一律妄呕,比如反射慢、反射過(guò)程中頻繁的創(chuàng)建對(duì)象占用更多內(nèi)存嗽测、頻繁的觸發(fā) GC 等等绪励。那么反射慢多少?反射會(huì)占用多少內(nèi)存?創(chuàng)建 1 個(gè)對(duì)象或者創(chuàng)建 10 萬(wàn)個(gè)對(duì)象耗時(shí)多少疏魏?單次反射或者 10 萬(wàn)次反射耗時(shí)多少停做?在我們的腦海中沒有一個(gè)直觀的概念,而今天這篇文章將會(huì)告訴你蠢护。
這篇文章雅宾,設(shè)計(jì)了幾個(gè)常用的場(chǎng)景,一起討論一下反射是否真的很耗時(shí)葵硕?最后會(huì)以圖表的形式展示眉抬。
測(cè)試工具及方案
在開始之前我們需要定義一個(gè)反射類 Person。
class Person {
var age = 10
fun getName(): String {
return "I am DHL"
}
companion object {
fun getAddress(): String = "BJ"
}
}
針對(duì)上面的測(cè)試類懈凹,設(shè)計(jì)了以下幾個(gè)常用的場(chǎng)景蜀变,驗(yàn)證反射前后的耗時(shí)。
- 創(chuàng)建對(duì)象
- 方法調(diào)用
- 屬性調(diào)用
- 伴生對(duì)象
測(cè)試工具及代碼:
JMH (Java Microbenchmark Harness)介评,這是 Oracle 開發(fā)的一個(gè)基準(zhǔn)測(cè)試工具库北,他們比任何人都了解 JIT 以及 JVM 的優(yōu)化對(duì)測(cè)試過(guò)程中的影響,所以使用這個(gè)工具可以盡可能的保證結(jié)果的可靠性们陆。
基準(zhǔn)測(cè)試是測(cè)試應(yīng)用性能的一種方法寒瓦,在特定條件下對(duì)某一對(duì)象的性能指標(biāo)進(jìn)行測(cè)試
本文的測(cè)試代碼已經(jīng)上傳到 github 倉(cāng)庫(kù) KtPractice 歡迎前往查看。
github 倉(cāng)庫(kù) KtPractice:
https://github.com/hi-dhl/KtPractice
為什么使用 JMH
因?yàn)?JVM 會(huì)對(duì)代碼做各種優(yōu)化坪仇,如果只是在代碼前后打印時(shí)間戳杂腰,這樣計(jì)算的結(jié)果是不置信的,因?yàn)楹雎粤?JVM 在執(zhí)行過(guò)程中椅文,對(duì)代碼進(jìn)行優(yōu)化產(chǎn)生的影響喂很。而 JMH 會(huì)盡可能的減少這些優(yōu)化對(duì)最終結(jié)果的影響。
測(cè)試方案
- 在單進(jìn)程皆刺、單線程中少辣,針對(duì)以上四個(gè)場(chǎng)景,每個(gè)場(chǎng)景測(cè)試五輪羡蛾,每輪循環(huán) 10 萬(wàn)次漓帅,計(jì)算它們的平均值
- 在執(zhí)行之前,需要對(duì)代碼進(jìn)行預(yù)熱痴怨,預(yù)熱不會(huì)作為最終結(jié)果煎殷,預(yù)熱的目的是為了構(gòu)造一個(gè)相對(duì)穩(wěn)定的環(huán)境,保證結(jié)果的可靠性腿箩。因?yàn)?JVM 會(huì)對(duì)執(zhí)行頻繁的代碼,嘗試編譯為機(jī)器碼劣摇,從而提高執(zhí)行速度珠移。而預(yù)熱不僅包含編譯為機(jī)器碼,還包含 JVM 各種優(yōu)化算法,盡量減少 JVM 的優(yōu)化钧惧,構(gòu)造一個(gè)相對(duì)穩(wěn)定的環(huán)境暇韧,降低對(duì)結(jié)果造成的影響。
- JMH 提供 Blackhole浓瞪,通過(guò) Blackhole 的 consume 來(lái)避免 JIT 帶來(lái)的優(yōu)化
Kotlin 和 Java 的反射機(jī)制
本文測(cè)試代碼全部使用 Kotlin懈玻,Koltin 是完美兼容 Java 的,所以同樣也可以使用 Java 的反射機(jī)制乾颁,但是 Kotlin 自己也封裝了一套反射機(jī)制涂乌,并不是用來(lái)取代 Java 的,是 Java 的增強(qiáng)版英岭,因?yàn)?Kotlin 有自己的語(yǔ)法特點(diǎn)比如擴(kuò)展方法 湾盒、伴生對(duì)象 、可空類型的檢查等等诅妹,如果想使用 Kotlin 反射機(jī)制罚勾,需要引入以下庫(kù)。
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
在開始分析吭狡,我們需要對(duì)比 Java 了解一下 Kotlin 反射基本語(yǔ)法尖殃。
- kotlin 的 KClass 對(duì)應(yīng) Java 的 Class,我們可以通過(guò)以下方式完成 KClass 和 Class 之間互相轉(zhuǎn)化
// 獲取 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 對(duì)應(yīng) Java 的 Field划煮,Java 的 Field 有 getter/setter 方法送丰,但是在 Kotlin 中沒有 Field,分為了 KProperty 和 KMutableProperty般此,當(dāng)變量用 val 聲明的時(shí)候蚪战,即屬性為 KProperty,如果變量用 var 聲明的時(shí)候铐懊,即屬性為 KMutableProperty
// Java 的獲取方式
Person().javaClass.getDeclaredField("age")
// Koltin 的獲取方式
Person::class.declaredMemberProperties.find { it.name == "age" }
- 在 Kotlin 中 函數(shù) 邀桑、屬性 以及 構(gòu)造函數(shù) 的超類型都是 KCallable,對(duì)應(yīng)的子類型是 KFunction (函數(shù)科乎、構(gòu)造方法等等) 和 KProperty / KMutableProperty (屬性)壁畸,而 Kotlin 中的 KCallable 對(duì)應(yīng) Java 的 AccessibleObject, 其子類型分別是 Method 、 Field 茅茂、 Constructor
// Java
Person().javaClass.getConstructor().newInstance() // 構(gòu)造方法
Person().javaClass.getDeclaredMethod("getName") // 成員方法
// Kotlin
Person::class.primaryConstructor?.call() // 構(gòu)造方法
Person::class.declaredFunctions.find { it.name == "getName" } // 成員方法
無(wú)論是使用 Java 還是 Kotlin 最終測(cè)試出來(lái)的結(jié)論都是一樣的捏萍,了解完基本反射語(yǔ)法之后,我們分別測(cè)試上述四種場(chǎng)景反射前后的耗時(shí)空闲。
創(chuàng)建對(duì)象
正常創(chuàng)建對(duì)象
@Benchmark
fun createInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person())
}
}
五輪測(cè)試平均耗時(shí) 0.578 ms/op 令杈。需要重點(diǎn)注意,這里使用了 JMH 提供 Blackhole碴倾,通過(guò) Blackhole 的 consume() 方法來(lái)避免 JIT 帶來(lái)的優(yōu)化, 讓結(jié)果更加接近真實(shí)逗噩。
在對(duì)象創(chuàng)建過(guò)程中掉丽,會(huì)先檢查類是否已經(jīng)加載,如果類已經(jīng)加載了异雁,會(huì)直接為對(duì)象分配空間捶障,其中最耗時(shí)的階段其實(shí)是類的加載過(guò)程(加載->驗(yàn)證->準(zhǔn)備->解析->初始化)。
通過(guò)反射創(chuàng)建對(duì)象
@Benchmark
fun createReflectInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person::class.primaryConstructor?.call())
}
}
五輪測(cè)試平均耗時(shí) 4.710 ms/op纲刀,是正常創(chuàng)建對(duì)象的 9.4 倍项炼,這個(gè)結(jié)果是很驚人,如果將中間操作(獲取構(gòu)造方法)從循環(huán)中提取出來(lái)示绊,那么結(jié)果會(huì)怎么樣呢锭部。
反射優(yōu)化
@Benchmark
fun createReflectInstanceAccessibleTrue(bh: Blackhole) {
val constructor = Person::class.primaryConstructor
for (index in 0 until 100_000) {
bh.consume(constructor?.call())
}
}
正如你所見,我將中間操作(獲取構(gòu)造方法)從循環(huán)中提取出來(lái)耻台,五輪測(cè)試平均耗時(shí) 1.018 ms/op空免,速度得到了很大的提升,相比反射優(yōu)化前速度提升了 4.7 倍盆耽,但是如果我們?cè)趯踩珯z查功能關(guān)掉呢蹋砚。
constructor?.isAccessible = true
isAccessible 是用來(lái)判斷是否需要進(jìn)行安全檢査,設(shè)置為 true 表示關(guān)掉安全檢查摄杂,將會(huì)減少安全檢査產(chǎn)生的耗時(shí)坝咐,五輪測(cè)試平均耗時(shí) 0.943 ms/op,反射速度進(jìn)一步提升了析恢。
幾輪測(cè)試最后的結(jié)果如下圖示墨坚。
方法調(diào)用
正常調(diào)用
@Benchmark
fun callMethod(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh.consume(person.getName())
}
}
五輪測(cè)試平均耗時(shí) 0.422 ms/op。
反射調(diào)用
@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))
}
}
五輪測(cè)試平均耗時(shí) 10.533 ms/op映挂,是正常調(diào)用的 26 倍泽篮。如果我們將中間操作(獲取 getName 代碼)從循環(huán)中提取出來(lái),結(jié)果會(huì)怎么樣呢柑船。
反射優(yōu)化
@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 代碼)從循環(huán)中提取出來(lái)了帽撑,五輪測(cè)試平均耗時(shí) 0.844 ms/op,速度得到了很大的提升鞍时,相比反射優(yōu)化前速度提升了 13 倍亏拉,如果在將安全檢查關(guān)掉呢。
method?.isAccessible = true
五輪測(cè)試平均耗時(shí) 0.687 ms/op逆巍,反射速度進(jìn)一步提升了及塘。
幾輪測(cè)試最后的結(jié)果如下圖示。
屬性調(diào)用
正常調(diào)用
@Benchmark
fun callPropertie(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh.consume(person.age)
}
}
五輪測(cè)試平均耗時(shí) 0.241 ms/op 锐极。
反射調(diào)用
@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))
}
}
五輪測(cè)試平均耗時(shí) 12.432 ms/op笙僚,是正常調(diào)用的 62 倍,然后我們將中間操作(獲取屬性的代碼)從循環(huán)中提出來(lái)灵再。
反射優(yōu)化
@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))
}
}
將中間操作(獲取屬性的代碼)從循環(huán)中提出來(lái)之后味咳,五輪測(cè)試平均耗時(shí) 1.362 ms/op庇勃,速度得到了很大的提升,相比反射優(yōu)化前速度提升了 8 倍槽驶,我們?cè)趯踩珯z查關(guān)掉,看一下結(jié)果鸳兽。
propertie?.isAccessible = true
五輪測(cè)試平均耗時(shí) 1.202 ms/op掂铐,反射速度進(jìn)一步提升了。
幾輪測(cè)試最后的結(jié)果如下圖示揍异。
伴生對(duì)象
正常調(diào)用
@Benchmark
fun callCompaion(bh: Blackhole) {
for (index in 0 until 100_000) {
bh.consume(Person.getAddress())
}
}
五輪測(cè)試平均耗時(shí) 0.470 ms/op 全陨。
反射調(diào)用
@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))
}
}
五輪測(cè)試平均耗時(shí) 5.661 ms/op,是正常調(diào)用的 11 倍衷掷,然后我們?cè)诳匆幌聦⒅虚g操作(獲取 getAddress 代碼)從循環(huán)中提出來(lái)的結(jié)果辱姨。
反射優(yōu)化
@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 代碼)從循環(huán)中提出來(lái),五輪測(cè)試平均耗時(shí) 0.840 ms/op戚嗅,速度得到了很大的提升雨涛,相比反射優(yōu)化前速度提升了 7 倍,現(xiàn)在我們?cè)趯踩珯z查關(guān)掉懦胞。
compaion?.isAccessible = true
五輪測(cè)試平均耗時(shí) 0.702 ms/op替久,反射速度進(jìn)一步提升了。
幾輪測(cè)試最后的結(jié)果如下圖所示躏尉。
總結(jié)
我們對(duì)比了四種常用的場(chǎng)景: 創(chuàng)建對(duì)象蚯根、方法調(diào)用、屬性調(diào)用胀糜、伴生對(duì)象颅拦。分別測(cè)試了反射前后的耗時(shí),最后匯總一下五輪 10 萬(wàn)次測(cè)試平均值教藻。
正常調(diào)用 | 反射 | 反射優(yōu)化后 | 反射優(yōu)化后關(guān)掉安全檢查 | |
---|---|---|---|---|
創(chuàng)建對(duì)象 | 0.578 ms/op | 4.710 ms/op | 1.018 ms/op | 0.943 ms/op |
方法調(diào)用 | 0.422 ms/op | 10.533 ms/op | 0.844 ms/op | 0.687 ms/op |
屬性調(diào)用 | 0.241 ms/op | 12.432 ms/op | 1.362 ms/op | 1.202 ms/op |
伴生對(duì)象 | 0.470 ms/op | 5.661 ms/op | 0.840 ms/op | 0.702 ms/op |
每個(gè)場(chǎng)景反射前后的耗時(shí)如下圖所示距帅。
在我們的印象中,反射就是惡魔怖竭,影響會(huì)非常大锥债,但是從上面的表格看來(lái),反射確實(shí)會(huì)有一定的影響痊臭,但是如果我們合理使用反射哮肚,優(yōu)化后的反射結(jié)果并沒有想象的那么大,這里有幾個(gè)建議广匙。
- 在頻繁的使用反射的場(chǎng)景中允趟,將反射中間操作提取出來(lái)緩存好,下次在使用反射直接從緩存中取即可
- 關(guān)掉安全檢查鸦致,可以進(jìn)一步提升性能
最后我們?cè)诳匆幌聠未蝿?chuàng)建對(duì)象和單次反射創(chuàng)建對(duì)象的耗時(shí)潮剪,如下圖所示涣楷。
Score 表示結(jié)果,Error 表示誤差范圍抗碰,在考慮誤差的情況下狮斗,它們的耗時(shí)差距在 微妙別以內(nèi)。
當(dāng)然根據(jù)設(shè)備的不同(高端機(jī)弧蝇、低端機(jī))碳褒,還有系統(tǒng)、復(fù)雜的類等等因素看疗,反射所產(chǎn)生的影響也是不同的沙峻。反射在實(shí)際項(xiàng)目中應(yīng)用的非常的廣泛,很多設(shè)計(jì)和開發(fā)都和反射有關(guān)两芳,比如通過(guò)反射去調(diào)用字節(jié)碼文件摔寨、調(diào)用系統(tǒng)隱藏 Api、動(dòng)態(tài)代理的設(shè)計(jì)模式怖辆,Android 逆向是复、著名的 Spring 框架、各類 Hook 框架等等疗隶。