java編程中,使用反射來加強靈活性(如各種框架)、某些抽象(如各種框架)及減小樣板代碼(如Java Bean)。
所以,反射在實際的java項目中被大量使用。java
因爲項目裏存在反射的性能瓶頸,使用的是ReflectASM高性能反射庫來優化。
所以,在空閒時間研究了下的這個庫,並作了簡單的Beachmark。python
<!--more-->git
ReflectASM是使用字節碼生成來增強反射的性能。
反射包含多種反射,這個庫很簡單,它提供的特性則是:github
這三種也偏偏是實際使用中最多的,且在特殊場景下也容易產生性能問題。編程
舉個例子,使用MethodAccess來反射調用類的函數:緩存
Person person = new Person(); MethodAccess m = MethodAccess.get(Person.class); Object value = m.invoke(person, "getName");
更多的例子參考官方文檔,這個庫自己就不大,就幾個類。app
static public MethodAccess get (Class type) { ArrayList<Method> methods = new ArrayList<Method>(); boolean isInterface = type.isInterface(); if (!isInterface) { Class nextClass = type; while (nextClass != Object.class) { addDeclaredMethodsToList(nextClass, methods); nextClass = nextClass.getSuperclass(); } } else { recursiveAddInterfaceMethodsToList(type, methods); } int n = methods.size(); String[] methodNames = new String[n]; Class[][] parameterTypes = new Class[n][]; Class[] returnTypes = new Class[n]; for (int i = 0; i < n; i++) { Method method = methods.get(i); methodNames[i] = method.getName(); parameterTypes[i] = method.getParameterTypes(); returnTypes[i] = method.getReturnType(); } String className = type.getName(); String accessClassName = className + "MethodAccess"; if (accessClassName.startsWith("java.")) accessClassName = "reflectasm." + accessClassName; Class accessClass; AccessClassLoader loader = AccessClassLoader.get(type); synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch (ClassNotFoundException ignored) { String accessClassNameInternal = accessClassName.replace('.', '/'); String classNameInternal = className.replace('.', '/'); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); MethodVisitor mv; /* ... 字節碼生成 */ byte[] data = cw.toByteArray(); accessClass = loader.defineClass(accessClassName, data); } } try { MethodAccess access = (MethodAccess)accessClass.newInstance(); access.methodNames = methodNames; access.parameterTypes = parameterTypes; access.returnTypes = returnTypes; return access; } catch (Throwable t) { throw new RuntimeException("Error constructing method access class: " + accessClassName, t); } }
大體邏輯爲:框架
因爲裏面包含字節碼生成操做,因此相對來講這個函數是比較耗時的。
咱們來分析一下,若是第二次調用對相同的類調用MethodAccess.get()
方法,會不會好一些?
注意到:ide
synchronized (loader) { try { accessClass = loader.loadClass(accessClassName); } catch { /* ... */ } }
所以,若是這個動態生成的MethodAccess類已經生成過,第二次調用MethodAccess.get
是不會操做字節碼生成的。
可是,前面的一大堆準備反射信息的操做依然會被執行。因此,若是在代碼中封裝這樣的一個函數試圖使用ReflectASM庫:函數
Object reflectionInvoke(Object bean, String methodName) { MethodAccess m = MethodAccess.get(bean.getClass()); return m.invoke(bean, methodName); }
那麼每次反射調用前都得執行這麼一大坨準備反射信息的代碼,實際上還不如用原生反射呢。這個後面會有Beachmark。
爲何不在找不到動態生成的MethodAccess類時(即第一次調用)時,再準備反射信息?這個得問做者。
那麼那個動態生成的類的內部究竟是什麼?
因爲這個類是動態生成的,因此獲取它的定義比較麻煩。
一開始我試圖尋找java的ClassLoader的API獲取它的字節碼,可是彷佛沒有這種API。
後來,我想了一個辦法,直接在MethodAccess.get
裏面的這行代碼打斷點:
byte[] data = cw.toByteArray();
經過idea的調試器把data
的內容複製出來。可是這又遇到一個問題,data是二進制內容,根本複製不出來。
一個一年要400美刀的IDE,爲啥不能作的貼心一點啊?
既然是二進制內容,那麼只能設法將其編碼成文本再複製了。經過idea調試器自定義view的功能,將其編碼成base64後複製了出來。
而後,搞個python小腳本將其base64解碼回.class文件:
#!/usr/bin/env python3 import base64 with open("tmp.txt", "rb") as fi, open("tmp.class", "wb") as fo: base64.decode(fi, fo)
反編譯.class文件,獲得:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package io.github.frapples.javademoandcookbook.commonutils.entity; import com.esotericsoftware.reflectasm.MethodAccess; public class PointMethodAccess extends MethodAccess { public PointMethodAccess() { } public Object invoke(Object var1, int var2, Object... var3) { Point var4 = (Point)var1; switch(var2) { case 0: return var4.getX(); case 1: var4.setX((Integer)var3[0]); return null; case 2: return var4.getY(); case 3: var4.setY((Integer)var3[0]); return null; case 4: return var4.toString(); case 5: return Point.of((Integer)var3[0], (Integer)var3[1], (String)var3[2]); default: throw new IllegalArgumentException("Method not found: " + var2); } } }
能夠看到,生成的invoke方法中,直接根據索引使用switch直接調用。
因此,只要使用得當,性能媲美原生調用是沒有什麼問題的。
來看invoke
方法內具體作了哪些操做:
abstract public Object invoke (Object object, int methodIndex, Object... args); /** Invokes the method with the specified name and the specified param types. */ public Object invoke (Object object, String methodName, Class[] paramTypes, Object... args) { return invoke(object, getIndex(methodName, paramTypes), args); } /** Invokes the first method with the specified name and the specified number of arguments. */ public Object invoke (Object object, String methodName, Object... args) { return invoke(object, getIndex(methodName, args == null ? 0 : args.length), args); } /** Returns the index of the first method with the specified name. */ public int getIndex (String methodName) { for (int i = 0, n = methodNames.length; i < n; i++) if (methodNames[i].equals(methodName)) return i; throw new IllegalArgumentException("Unable to find non-private method: " + methodName); }
若是經過函數名稱調用函數(即調用invoke(Object, String, Class[], Object...)
,
則MethodAccess
是先遍歷全部函數名稱拿到索引,而後根據索引調用對應方法(即調用虛函數invoke(Object, int, Object...)
,
其實是經過多態調用字節碼動態生成的子類的對應函數。
若是被反射調用的類的函數不少,則這個遍歷操做帶來的性能損失不能忽略。
因此,性能要求高的場合,應該預先經過getIndex
方法提早得到索引,而後後面便可以直接使用invoke(Object, int, Object...)
來調用。
談這種細粒度操做級別的性能問題,最有說服力的就是實際測試數據了。
下面,Talk is cheap, show you my beachmark.
首先是相關環境:
操做系統版本: elementary OS 0.4.1 Loki 64-bit
CPU: 雙核 Intel® Core™ i5-7200U CPU @ 2.50GHz
JMH基準測試框架版本: 1.21
JVM版本: JDK 1.8.0_181, OpenJDK 64-Bit Server VM, 25.181-b13
Benchmark Mode Cnt Score Error Units // 經過MethodHandle調用。預先獲得某函數的MethodHandle ReflectASMBenchmark.javaMethodHandleWithInitGet thrpt 5 122.988 ± 4.240 ops/us // 經過java反射調用。緩存獲得的Method對象 ReflectASMBenchmark.javaReflectWithCacheGet thrpt 5 11.877 ± 2.203 ops/us // 經過java反射調用。預先獲得某函數的Method對象 ReflectASMBenchmark.javaReflectWithInitGet thrpt 5 66.702 ± 11.154 ops/us // 經過java反射調用。每次調用都先取得Method對象 ReflectASMBenchmark.javaReflectWithOriginGet thrpt 5 3.654 ± 0.795 ops/us // 直接調用 ReflectASMBenchmark.normalCall thrpt 5 1059.926 ± 99.724 ops/us // ReflectASM經過索引調用。預先取得MethodAccess對象,預先取得某函數的索引 ReflectASMBenchmark.reflectAsmIndexWithCacheGet thrpt 5 639.051 ± 47.750 ops/us // ReflectASM經過函數名調用,緩存獲得的MethodAccess對象 ReflectASMBenchmark.reflectAsmWithCacheGet thrpt 5 21.868 ± 1.879 ops/us // ReflectASM經過函數名調用,預先獲得的MethodAccess ReflectASMBenchmark.reflectAsmWithInitGet thrpt 5 53.370 ± 0.821 ops/us // ReflectASM經過函數名調用,每次調用都取得MethodAccess ReflectASMBenchmark.reflectAsmWithOriginGet thrpt 5 0.593 ± 0.005 ops/us
能夠看到,每次調用都來一次MethodAccess.get
,性能是最慢的,時間消耗是java原生調用的6倍,不如用java原生調用。
最快的則是預先取得MethodAccess和函數的索引並用索引來調用。其時間消耗僅僅是直接調用的2倍不到。
基準測試代碼見:
https://github.com/frapples/j...
jmh框架十分專業,在基準測試前會作複雜的預熱過程以減小環境、優化等影響,基準測試也儘量經過合理的迭代次數等方式來減少偏差。
因此,在默認的迭代次數、預熱次數下,跑一次基準測試的時間不短,CPU呼呼的轉。。。
在使用ReflectASM對某類進行反射調用時,須要預先生成或獲取字節碼動態生成的MethodAccess子類對象。
這一操做是很是耗時的,因此正確的使用方法應該是:
若是不這樣作,這個ReflectASM用的沒有任何意義,性能還不如java的原生反射。
若是想進一步提高性能,那麼還應該避免使用函數的字符串名稱來調用,而是在耗時的函數啓動前,預先獲取函數名稱對應的整數索引。在後面的耗時的函數,使用這個整數索引進行調用。