揭秘反射真的很耗時(shí)嗎峡懈,射 10 萬次用時(shí)多久

全文分為 視頻版文字版

  • 文字版: 側(cè)重于細(xì)節(jié)上的知識(shí)點(diǎn)更多与斤、更加詳細(xì)
  • 視頻版: 通過動(dòng)畫展示講解肪康,更加的清楚、直觀

視頻版本地址:https://b23.tv/Hprua24

無論是在面試過程中撩穿,還是看網(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拢驾,我們可以通過以下方式完成 KClassClass 之間互相轉(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 的 Fieldgetter/setter 方法,但是在 Kotlin 中沒有 Field繁疤,分為了 KPropertyKMutableProperty咖为,當(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,通過 Blackholeconsume() 方法來避免 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)贊 良瞧、 收藏分享 給身邊的朋友训唱。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末褥蚯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子况增,更是在濱河造成了極大的恐慌遵岩,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巡通,死亡現(xiàn)場(chǎng)離奇詭異尘执,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宴凉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門誊锭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弥锄,你說我怎么就攤上這事丧靡。” “怎么了籽暇?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵温治,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我戒悠,道長(zhǎng)熬荆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任绸狐,我火速辦了婚禮卤恳,結(jié)果婚禮上累盗,老公的妹妹穿的比我還像新娘。我一直安慰自己突琳,他們只是感情好若债,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拆融,像睡著了一般蠢琳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镜豹,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天挪凑,我揣著相機(jī)與錄音,去河邊找鬼逛艰。 笑死躏碳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的散怖。 我是一名探鬼主播菇绵,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼镇眷!你這毒婦竟也來了咬最?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤欠动,失蹤者是張志新(化名)和其女友劉穎永乌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體具伍,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翅雏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了人芽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片望几。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萤厅,靈堂內(nèi)的尸體忽然破棺而出橄抹,到底是詐尸還是另有隱情,我是刑警寧澤惕味,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布楼誓,位于F島的核電站,受9級(jí)特大地震影響名挥,放射性物質(zhì)發(fā)生泄漏疟羹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阁猜。 院中可真熱鬧,春花似錦蹋艺、人聲如沸剃袍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽民效。三九已至,卻和暖如春涛救,著一層夾襖步出監(jiān)牢的瞬間畏邢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工检吆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舒萎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓蹭沛,卻偏偏與公主長(zhǎng)得像臂寝,于是被迫代替她去往敵國(guó)和親俘侠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子状您,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容