java反射性能優化

反射真的慢嗎?
是的,很慢!

下圖是一億次循環的耗時:

直接調用 100000000 times using 36 ms
原生反射(只invoke) 100000000 times using 325 ms
原生反射(只getMethod) 100000000 times using 11986 ms
原生反射(緩存Method) 100000000 times using 319 ms
原生反射(沒有緩存Method) 100000000 times using 12169 ms
reflectAsm反射優化(緩存Method) 100000000 times using 43 ms
reflectAsm反射優化(沒有緩存Method) 100000000 times using 131788 ms
沒有一個可以比 直接調用 更快的。

原生反射(沒有緩存Method) 大概比 直接調用 慢了 340倍
原生反射(緩存Method) 大概比 直接調用 慢了 9倍
怎麼優化速度?
反射的速度差異只在大量連續使用才能明顯看出來,理論上100萬次纔會說反射很慢,對於一個單進單出的請求來說,反射與否根本差不了多少。

這樣就沒必要優化了嗎,並不是。

事實上各大框架註解,甚至業務系統中都在使用反射,不能因爲慢就不用了。
在後臺Controller中序列化請求響應信息大量使用註解,高併發就意味着連續百萬級別調用反射成爲可能,各大MVC框架都會着手解決這個問題,優化反射。

反射核心的是getMethod和invoke了,分析下兩者的耗時差距,在一億次循環下的耗時。

Method getName = SimpleBean.class.getMethod("getName");
getName.invoke(bean);

原生反射(只invoke) 100000000 times using 221 ms
原生反射(只getMethod) 100000000 times using 12849 ms
優化思路1:緩存Method,不重複調用getMethod
證明getMethod很耗時,所以說我們要優先優化getMethod,看看爲什麼卡?

Method getName = SimpleBean.class.getMethod("getName");
//查看源碼
Method res =  privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates);
//再看下去
private native Field[]       getDeclaredFields0(boolean publicOnly);
private native Method[]      getDeclaredMethods0(boolean publicOnly);
private native Constructor<T>[] getDeclaredConstructors0(boolean publicOnly);
private native Class<?>[]   getDeclaredClasses0();

getMethod最後直接調用native方法,無解了。想複寫優化getMethod是不可能的了,官方沒毛病。
但是我們可以不需要每次都getMethod啊,我們可以緩存到redis,或者放到Spring容器中,就不需要每次都拿了。

//通過Java Class類自帶的反射獲得Method測試,僅進行一次method獲取
  @Test
    public void javaReflectGetOnly() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Method getName = SimpleBean.class.getMethod("getName");
        Stopwatch watch = Stopwatch.createStarted();
        for (long i = 0; i < times; i++) {
            getName.invoke(bean);
        }
        watch.stop();
        String result = String.format(formatter, "原生反射+緩存Method", times, watch.elapsed(TimeUnit.MILLISECONDS));
        System.out.println(result);
    }

原生反射(緩存Method) 100000000 times using 319 ms
原生反射(沒有緩存Method) 100000000 times using 12169 ms
緩存Method大概快了38倍,離原生調用還差個9倍,所以我們繼續優化invoke。

優化思路2:使用reflectAsm,讓invoke變成直接調用
我們看下invoke的源碼:

getName.invoke(bean);
//查看源碼
private static native Object invoke0(Method var0, Object var1, Object[] var2);

尷尬,最後還是native方法,依然沒毛病。
invoke不像getMethod可以緩存起來重複用,沒法優化。

所以這裏需要引入ASM,並做了個工具庫reflectAsm:

“ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,能夠改變類行爲,分析類信息,甚至能夠根據用戶要求生成新類。”

使用如下:

MethodAccess methodAccess = MethodAccess.get(SimpleBean.class);
methodAccess.invoke(bean, "getName");

//看看MethodAccess.get(SimpleBean.class)源碼,使用了反射的getMethod

Method[] declaredMethods = type.getDeclaredMethods();

invoke是沒辦法優化的,也沒辦法做到像直接調用那麼快。所以大佬們腦洞大開,不用反射的invoke了。原理如下:

借反射的getDeclaredMethods獲取SimpleBean.class的所有方法,然後動態生成一個繼承於MethodAccess 的子類SimpleBeanMethodAccess,動態生成一個Class文件並load到JVM中。
SimpleBeanMethodAccess中所有方法名建立index索引,index跟方法名是映射的,根據方法名獲得index,SimpleBeanMethodAccess內部建立的switch直接分發執行相應的代碼,這樣methodAccess.invoke的時候,實際上是直接調用。
實際上reflectAsm是有個致命漏洞的,因爲要生成文件,還得load進JVM,所以reflectAsm的getMethod特別慢:

reflectAsm反射優化(沒有緩存Method) 100000000 times using 131788 ms
雖然getMethod很慢,但是invoke的速度是到達了直接調用的速度了。

如果能夠緩存method,那麼reflectAsm的速度跟直接調用一樣,而且能夠使用反射!

直接調用 100000000 times using 36 ms
reflectAsm反射優化(緩存Method) 100000000 times using 43 ms
這其中差的7ms,是reflectAsm生成一次Class文件的損耗。
下面是反射優化的測試樣例:

//通過高性能的ReflectAsm庫進行測試,僅進行一次methodAccess獲取
@Test
public void reflectAsmGetOnly() {
        MethodAccess methodAccess = MethodAccess.get(SimpleBean.class);
        Stopwatch watch = Stopwatch.createStarted();
        for (long i = 0; i < times; i++) {
            methodAccess.invoke(bean, "getName");
        }
        watch.stop();
        String result = String.format(formatter, "reflectAsm反射優化+緩存Method", times, watch.elapsed(TimeUnit.MILLISECONDS));
        System.out.println(result);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章