全文分為 視頻版 和 文字版,
- 文字版: 側(cè)重于細(xì)節(jié)上的知識(shí)點(diǎn)更多与斤、更加詳細(xì)
- 視頻版: 通過動(dòng)畫展示講解肪康,更加的清楚、直觀
無論是在面試過程中撩穿,還是看網(wǎng)絡(luò)上各種技術(shù)文章磷支,只要提到反射,不可避免都會(huì)提到一個(gè)問題冗锁,反射會(huì)影響性能嗎?影響有多大嗤栓?如果在寫業(yè)務(wù)代碼的時(shí)候冻河,你用到了反射,都會(huì)被 review 人發(fā)出靈魂拷問茉帅,為什么要用反射叨叙,有沒有其它的解決辦法。
而網(wǎng)上的答案都是千篇一律堪澎,比如反射慢擂错、反射過程中頻繁的創(chuàng)建對(duì)象占用更多內(nèi)存、頻繁的觸發(fā) GC 等等樱蛤。那么反射慢多少钮呀?反射會(huì)占用多少內(nèi)存?創(chuàng)建 1 個(gè)對(duì)象或者創(chuàng)建 10 萬個(gè)對(duì)象耗時(shí)多少昨凡?單次反射或者 10 萬次反射耗時(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è)試過程中的影響奸笤,所以使用這個(gè)工具可以盡可能的保證結(jié)果的可靠性。
基準(zhǔn)測(cè)試是測(cè)試應(yīng)用性能的一種方法哼鬓,在特定條件下對(duì)某一對(duì)象的性能指標(biāo)進(jìn)行測(cè)試
本文的測(cè)試代碼已經(jīng)上傳到 github 倉庫 KtPractice 歡迎前往查看监右。
github 倉庫 KtPractice: https://github.com/hi-dhl/KtPractice
為什么使用 JMH
因?yàn)?JVM 會(huì)對(duì)代碼做各種優(yōu)化,如果只是在代碼前后打印時(shí)間戳异希,這樣計(jì)算的結(jié)果是不置信的健盒,因?yàn)楹雎粤?JVM 在執(zhí)行過程中,對(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 萬次授药,計(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,通過 Blackhole 的 consume 來避免 JIT 帶來的優(yōu)化
Kotlin 和 Java 的反射機(jī)制
本文測(cè)試代碼全部使用 Kotlin虑鼎,Koltin 是完美兼容 Java 的辱匿,所以同樣也可以使用 Java 的反射機(jī)制键痛,但是 Kotlin 自己也封裝了一套反射機(jī)制,并不是用來取代 Java 的匾七,是 Java 的增強(qiáng)版絮短,因?yàn)?Kotlin 有自己的語法特點(diǎn)比如擴(kuò)展方法 、伴生對(duì)象 昨忆、可空類型的檢查等等丁频,如果想使用 Kotlin 反射機(jī)制,需要引入以下庫邑贴。
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
在開始分析席里,我們需要對(duì)比 Java 了解一下 Kotlin 反射基本語法。
- kotlin 的
KClass
對(duì)應(yīng) Java 的Class
拢驾,我們可以通過以下方式完成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" } // 成員方法
無論是使用 Java 還是 Kotlin 最終測(cè)試出來的結(jié)論都是一樣的,了解完基本反射語法之后许昨,我們分別測(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
,通過 Blackhole
的 consume()
方法來避免 JIT 帶來的優(yōu)化, 讓結(jié)果更加接近真實(shí)拌喉。
在對(duì)象創(chuàng)建過程中速那,會(huì)先檢查類是否已經(jīng)加載,如果類已經(jīng)加載了尿背,會(huì)直接為對(duì)象分配空間端仰,其中最耗時(shí)的階段其實(shí)是類的加載過程(加載->驗(yàn)證->準(zhǔn)備->解析->初始化)。
通過反射創(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)中提取出來,那么結(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)中提取出來,五輪測(cè)試平均耗時(shí) 1.018 ms/op
臀稚,速度得到了很大的提升吝岭,相比反射優(yōu)化前速度提升了 4.7
倍,但是如果我們?cè)趯踩珯z查功能關(guān)掉呢吧寺。
constructor?.isAccessible = true
isAccessible
是用來判斷是否需要進(jìn)行安全檢査窜管,設(shè)置為 true
表示關(guān)掉安全檢查,將會(huì)減少安全檢査產(chǎn)生的耗時(shí)稚机,五輪測(cè)試平均耗時(shí) 0.943 ms/op
幕帆,反射速度進(jìn)一步提升了。
幾輪測(cè)試最后的結(jié)果如下圖示抒钱。
[圖片上傳失敗...(image-2c62f2-1652420323429)]
方法調(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)中提取出來仗扬,結(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)中提取出來了蕾额,五輪測(cè)試平均耗時(shí) 0.844 ms/op
早芭,速度得到了很大的提升,相比反射優(yōu)化前速度提升了 13
倍诅蝶,如果在將安全檢查關(guān)掉呢退个。
method?.isAccessible = true
五輪測(cè)試平均耗時(shí) 0.687 ms/op
,反射速度進(jìn)一步提升了调炬。
幾輪測(cè)試最后的結(jié)果如下圖示语盈。
[圖片上傳失敗...(image-11799e-1652420323429)]
屬性調(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)中提出來。
反射優(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)中提出來之后棘钞,五輪測(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é)果如下圖示渠鸽。
[圖片上傳失敗...(image-b14e19-1652420323429)]
伴生對(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)中提出來的結(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)中提出來丽蝎,五輪測(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é)果如下圖所示吧恃。
[圖片上傳失敗...(image-367e05-1652420323429)]
總結(jié)
我們對(duì)比了四種常用的場(chǎng)景: 創(chuàng)建對(duì)象、方法調(diào)用麻诀、屬性調(diào)用痕寓、伴生對(duì)象。分別測(cè)試了反射前后的耗時(shí)蝇闭,最后匯總一下五輪 10 萬次測(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í)如下圖所示。
[圖片上傳失敗...(image-de174e-1652420323429)]
在我們的印象中呻引,反射就是惡魔礼仗,影響會(huì)非常大,但是從上面的表格看來逻悠,反射確實(shí)會(huì)有一定的影響元践,但是如果我們合理使用反射,優(yōu)化后的反射結(jié)果并沒有想象的那么大童谒,這里有幾個(gè)建議单旁。
- 在頻繁的使用反射的場(chǎng)景中,將反射中間操作提取出來緩存好饥伊,下次在使用反射直接從緩存中取即可
- 關(guān)掉安全檢查象浑,可以進(jìn)一步提升性能
最后我們?cè)诳匆幌聠未蝿?chuàng)建對(duì)象和單次反射創(chuàng)建對(duì)象的耗時(shí),如下圖所示撵渡。
[圖片上傳失敗...(image-d621f2-1652420323429)]
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),比如通過反射去調(diào)用字節(jié)碼文件狼渊、調(diào)用系統(tǒng)隱藏 Api箱熬、動(dòng)態(tài)代理的設(shè)計(jì)模式,Android 逆向狈邑、著名的 Spring 框架城须、各類 Hook 框架等等。
文章中的代碼已經(jīng)上傳到 github 倉庫 KtPractice: https://github.com/hi-dhl/KtPractice
全文到這里就結(jié)束了米苹,感謝你的閱讀糕伐,如果有幫助,歡迎 在看
蘸嘶、 點(diǎn)贊
良瞧、 收藏
、 分享
給身邊的朋友训唱。