反射真的很耗時(shí)嗎滤灯?射10萬(wàn)次用時(shí)多久坪稽?

作者: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 框架等等疗隶。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末佑笋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子斑鼻,更是在濱河造成了極大的恐慌蒋纬,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坚弱,死亡現(xiàn)場(chǎng)離奇詭異蜀备,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)荒叶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門碾阁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人些楣,你說(shuō)我怎么就攤上這事脂凶。” “怎么了愁茁?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵蚕钦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我鹅很,道長(zhǎng)嘶居,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任促煮,我火速辦了婚禮邮屁,結(jié)果婚禮上整袁,老公的妹妹穿的比我還像新娘。我一直安慰自己佑吝,他們只是感情好坐昙,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著迹蛤,像睡著了一般民珍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗飒,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音陋桂,去河邊找鬼逆趣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嗜历,可吹牛的內(nèi)容都是我干的宣渗。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼梨州,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼痕囱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起暴匠,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鞍恢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后每窖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帮掉,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年窒典,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟆炊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瀑志,死狀恐怖涩搓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劈猪,我是刑警寧澤昧甘,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站岸霹,受9級(jí)特大地震影響疾层,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贡避,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一痛黎、第九天 我趴在偏房一處隱蔽的房頂上張望予弧。 院中可真熱鬧,春花似錦湖饱、人聲如沸掖蛤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蚓庭。三九已至,卻和暖如春仅仆,著一層夾襖步出監(jiān)牢的瞬間器赞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工墓拜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留港柜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓咳榜,卻偏偏與公主長(zhǎng)得像夏醉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涌韩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

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