寫在前面
? Java的反射在日常開發(fā)中還是經(jīng)常用到的技術(shù)點,這包括spring的Ioc,包括一些除cglib之外的bean copy(cglib采用asm動態(tài)生成字節(jié)碼來實現(xiàn)),然而在spring的ioc中,我們或許無法感知到,這是因為大部分類實例都是單例,只在容器啟動的時候加載一次,并在容器內(nèi)緩存它的實例。但是在業(yè)務(wù)code中的beancopy則不然。你會發(fā)現(xiàn)請求量大的情況下,很多線程棧都會在這個位置慢下來,并且消耗較高的cpu。這也就是反射慢引起的,那么反射為什么慢呢?下面我們就來一一揭曉
反射方法的調(diào)用case [v1]
public class InflactTest {
public static void targetMethod(){
//打印堆棧信息
new Exception().printStackTrace();
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> inflactTest = Class.forName("inflact.test.InflactTest");
Method method = inflactTest.getMethod("targetMethod");
method.invoke(null,null);
}
}
-
首先Class.forName屬于native方法,native方法就要經(jīng)過語言執(zhí)行層面轉(zhuǎn)換。也就是java到c再到j(luò)ava的切換。
-
而getMethod這個操作則會遍歷該類的公有方法,如果沒有命中,則還要去父類中查找。并且返回該method對象的一份copy。在查找成功之后,這份copy對象,則會占用堆空間,而無法進(jìn)行內(nèi)聯(lián)優(yōu)化,相反還會引起gc頻率的提高。對性能也是一份影響。
-
值得注意的是,以getMethod為代表,其中g(shù)etMethods和getDeclaredMethods等方法,都會進(jìn)行g(shù)etMethod相關(guān)的操作。所以要盡量避免在熱點代碼中使用該邏輯。
-
因大部分場景中,我們都會在程序中緩存對象實例本身,也就是說運(yùn)行過程中只會執(zhí)行一次,不屬于熱點操作,我們暫且不論。那么接下來我們只需要關(guān)注invoke方法本身即可。調(diào)用輸出如下所示

在圖中博主貼出了反射調(diào)用與普通調(diào)用的棧信息區(qū)別,位于紅色框中。那么可以肯定的是這些多出來的部分肯定是影響到反射慢的根本原因,那么jvm從底層都進(jìn)行了哪些操作呢?
反射的委派實現(xiàn)
-
在上一個case中,我們實現(xiàn)用一個方法的反射調(diào)用,相比于我們的正常執(zhí)行棧理解,多出來了三行code,那么,這三行code是在做什么呢?分析源碼不難發(fā)現(xiàn),其實在Method對象內(nèi)部維護(hù)了一個接口MethodAccessor,該接口有二個實現(xiàn)類,其中NativeMethodAccessorImpl用來實現(xiàn)本地native調(diào)用。而DelegatingMethodAccessorImpl顧名思義,是一個委派實現(xiàn)類,該方法將invoke操作委派給了native方法。
-
那么我們不禁發(fā)問,為什么要多此一舉呢?不能直接調(diào)用native方法嗎?
這里之所以抽象出來一個委派實現(xiàn),而不直接調(diào)用native實現(xiàn)方法,難道還有另外一種實現(xiàn)?
反射的動態(tài)實現(xiàn)
- 反射調(diào)用的第二個版本,在運(yùn)行第十五次的時候?qū)⑶袚Q為動態(tài)實現(xiàn)
/**
*
* 反射調(diào)用的第二個版本,在運(yùn)行第十五次的時候?qū)⑶袚Q為動態(tài)實現(xiàn)
* @author Nero
* @date 2019-08-06
* *@param: null
* @return
*/
public class InflactTestV2 {
public static void targetMethod(int i){
//打印堆棧信息
new Exception("index : " i).printStackTrace();
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> inflactTest = Class.forName("inflact.test.InflactTestV2");
Method method = inflactTest.getMethod("targetMethod",int.class);
for (int i= 0 ; i < 20 ; i ){
method.invoke(null,i);
}
}
}
-
運(yùn)行結(jié)果
![[外鏈圖片轉(zhuǎn)存失敗(img-PT4N30v8-1565325272261)(/Users/nero/Library/Application Support/typora-user-images/image-20190806215756431.png)]](http://image109.360doc.com/DownloadImg/2019/08/0913/168075237_2_20190809010343147)
-
動態(tài)實現(xiàn)的閾值
為了方便理解,博主將v2版本的執(zhí)行棧再次打印,在程序調(diào)用第16次的時候,調(diào)用棧更改成了GeneratedMethodAccessor1,而不再是native方法。這是因為jvm維護(hù)了一個閾值
-Dsun.reflect.inflationThreshold,默認(rèn)為15。當(dāng)反射native調(diào)用超過15次就會觸發(fā)jvm的動態(tài)生成字節(jié)碼,之后的操作,全部都會調(diào)用該動態(tài)實現(xiàn)。
動態(tài)實現(xiàn)與native實現(xiàn)相比,動態(tài)實現(xiàn)的效率要快的多,這是因為native的實現(xiàn)要在java語言層面切換到c語言,然后再次切換到j(luò)ava語言。
但是,因為動態(tài)實現(xiàn)第一次生成的時候要生成字節(jié)碼,而這個操作是比較耗時的。所以相比較起來單獨(dú)一次調(diào)用的時候native反而要比動態(tài)實現(xiàn)快的多。
-Dsun.reflect.noInflation=true 關(guān)閉反射的多重實現(xiàn)
? 上文中,我們講到一個切換實現(xiàn)方式的閾值,如果在業(yè)務(wù)code中調(diào)用了第三方j(luò)ar,在大量硬編碼變更的情況下,可以自己設(shè)置閾值。當(dāng)然也可以關(guān)閉反射的多重實現(xiàn),使得在第一次調(diào)用的時候就生成字節(jié)碼,在之后的調(diào)用中都是Java執(zhí)行棧自己的調(diào)用??梢栽趈vm中設(shè)置-Dsun.reflect.noInflation=true
invoke變長參數(shù)與自動裝箱

- 觀察invoke方法,public Object invoke(Object obj, Object… args),這個可變長參數(shù)在我們第二個版本它會發(fā)生什么呢? 看v2 code的bytecode 發(fā)現(xiàn)每一個調(diào)用便會調(diào)用ANEWARRAY,這相比直接調(diào)用的開銷可想而知
- 由于Object數(shù)組不支持基本類型,所以我們傳入的對象雖然為基本類型,但是依然會觸發(fā)每一次參數(shù)的自動裝箱。這也屬于我們直接調(diào)用之外的處理
- 以上的兩個處理除了額外處理占用的cpu耗時之外,還可能在堆上分配內(nèi)存,以此來增加gc的頻率。可以用虛擬機(jī)參數(shù) -XX:PrintGC 查看。
總結(jié)
-
本篇博文是博主基于極客時間鄭雨迪博士的文章總結(jié)筆記。其中由于逃逸分析和方法內(nèi)聯(lián)沒有完全吸收暫時不做討論。
-
文章鏈接:https://time./column/intro/108 [需付費(fèi)]
-
由于時間和篇幅關(guān)系,暫且沒有測試它們之間的性能差異。感興趣的同學(xué)可以使用openJdk提供的jmh工具測試性能差異。雖然jmh不是絕對嚴(yán)謹(jǐn)。但總比out.print好的多。
延伸
來源:https://www./content-4-383951.html
|