前言
在我們平時的工作或者面試中,都會經常遇到“反射”這個知識點,通過“反射”我們可以動態的獲取到物件的資訊以及靈活的呼叫物件方法等,但是在使用的同時又伴隨著另一種聲音的出現,那就是“反射”很慢,要少用。難道反射真的很慢?那跟我們平時正常建立物件呼叫方法比慢多少? 估計很多人都沒去測試過,只是”道聽途說“。下面我們就直接通過一些測試用例來直觀的感受一下”反射“。
正文
準備測試物件
下面先定義一個測試的類TestUser,只有id跟name屬性,以及它們的getter/setter方法,另外還有一個自定義的sayHi方法。
public class TestUser {
private Integer id;
private String name;
public String sayHi(){
return "hi";
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
複製程式碼
測試建立100萬個物件
// 通過普通方式建立TestUser物件
@Test
public void testCommon(){
long start = System.currentTimeMillis();
TestUser user = null;
int i = 0;
while(i<1000000){
++i;
user = new TestUser();
}
long end = System.currentTimeMillis();
System.out.println("普通物件建立耗時:"+(end - start ) + "ms");
}
//普通物件建立耗時:10ms
複製程式碼
// 通過反射方式建立TestUser物件
@Test
public void testReflexNoCache() throws Exception {
long start = System.currentTimeMillis();
TestUser user = null;
int i = 0;
while(i<1000000){
++i;
user = (TestUser) Class.forName("ReflexDemo.TestUser").newInstance();
}
long end = System.currentTimeMillis();
System.out.println("無快取反射建立物件耗時:"+(end - start ) + "ms");
}
//無快取反射建立物件耗時:926ms
複製程式碼
在上面這兩個測試方法中,筆者各自測了5次,把他們消耗的時間取了一個平均值,在輸出結果中可以看到一個是10ms,一個是926ms,在建立100W個物件的情況下,反射居然慢了90倍左右。wtf?差距居然這麼大?難道反射真的這麼慢?下面筆者換一種反射的姿勢,繼續測試一下,看看結果如何?
// 通過快取反射方式建立TestUser物件
@Test
public void testReflexWithCache() throws Exception {
long start = System.currentTimeMillis();
TestUser user = null;
Class rUserClass = Class.forName("RefleDemo.TestUser");
int i = 0;
while(i<1000000){
++i;
user = (TestUser) rUserClass.newInstance();
}
long end = System.currentTimeMillis();
System.out.println("通過快取反射建立物件耗時:"+(end - start ) + "ms");
}
//通過快取反射建立物件耗時:41ms
複製程式碼
咦?這種操作只需要41ms了,大大提高了反射建立物件的效率。為什麼會快這麼多呢?
其實通過程式碼我們可以發現,是Class.forName這個方法比較耗時,它實際上呼叫了一個本地方法,通過這個方法來要求JVM查詢並載入指定的類。所以我們在專案中使用的時候,可以把Class.forName返回的Class物件快取起來,下一次使用的時候直接從快取裡面獲取,這樣就極大的提高了獲取Class的效率。同理,在我們獲取Constructor、Method等物件的時候也可以快取起來使用,避免每次使用時再來耗費時間建立。
測試反射呼叫方法
@Test
public void testReflexMethod() throws Exception {
long start = System.currentTimeMillis();
Class testUserClass = Class.forName("RefleDemo.TestUser");
TestUser testUser = (TestUser) testUserClass.newInstance();
Method method = testUserClass.getMethod("sayHi");
int i = 0;
while(i<100000000){
++i;
method.invoke(testUser);
}
long end = System.currentTimeMillis();
System.out.println("反射呼叫方法耗時:"+(end - start ) + "ms");
}
//反射呼叫方法耗時:330ms
複製程式碼
@Test
public void testReflexMethod() throws Exception {
long start = System.currentTimeMillis();
Class testUserClass = Class.forName("RefleDemo.TestUser");
TestUser testUser = (TestUser) testUserClass.newInstance();
Method method = testUserClass.getMethod("sayHi");
int i = 0;
while(i<100000000){
++i;
method.setAccessible(true);
method.invoke(testUser);
}
long end = System.currentTimeMillis();
System.out.println("setAccessible=true 反射呼叫方法耗時:"+(end - start ) + "ms");
}
//setAccessible=true 反射呼叫方法耗時:188ms
複製程式碼
這裡我們反射呼叫sayHi方法1億次,在呼叫了method.setAccessible(true)後,發現快了將近一半。檢視API可以瞭解到,jdk在設定獲取欄位,呼叫方法的時候會執行安全訪問檢查,而此類操作會比較耗時,所以通過setAccessible(true)的方式可以關閉安全檢查,從而提升反射效率。
極致的反射
除了上面的手段,還有沒有什麼辦法可以更極致的使用反射呢?這裡介紹一個高效能反射工具包ReflectASM。它是通過位元組碼生成的方式來實現的反射機制,下面是一個跟java反射的效能比較。
這裡就不介紹它的用法了,有興趣的朋友可以直接傳送過去:github.com/EsotericSof…
結語
最後總結一下,為了更好的使用反射,我們應該在專案啟動的時候將反射所需要的相關配置及資料載入進記憶體中,在執行階段都從快取中取這些後設資料進行反射操作。大家也不用懼怕反射,虛擬機器在不斷的優化,只要我們方法用的對,它並沒有”傳聞“中的那麼慢,當我們對效能有極致追求的時候,可以考慮通過三方包,直接對位元組碼進行操作。
公眾號博文同步Github倉庫,有興趣的朋友可以幫忙給個Star哦,碼字不易,感謝支援。
推薦閱讀
《Java日誌正確使用姿勢》
《使用ConcurrentHashMap一定執行緒安全?》
《大白話搞懂什麼是同步/非同步/阻塞/非阻塞》
《論JVM爆炸的幾種姿勢及自救方法》
關注「深夜裡的程式猿」,分享最乾的乾貨