Java反射及性能詳細
我們今天不探討框架層面的內容,暫且認為90%的框架不存在無法容忍的性能問題。在做系統調優的過程中,面對隨處可見的invoke
調用,我的內心其實是比較抵觸的,倒不是說反射怎麼不好,對於優雅的源碼來說,反射必不可少,個人抵觸的原因主要是因為反射把真實的方法“隱藏”的很好,面對長長的線程棧比較頭大而已。而且我心裡一直有個大大的問號,反射到底存在哪些性能問題。
帶著這個疑惑,基於java最基本的反射使用,通過查看資料及源碼閱讀,有如下的總結和分享,歡迎交流和指正。
一、準備
註:本案例針對JDK1.8
測試代碼:
【TestRef.java】 public class TestRef { public static void main(String[] args) { try { Class<?> clazz = Class.forName("com.allen.commons.entity.CommonTestEntity"); Object refTest = clazz.newInstance(); Method method = clazz.getMethod("defaultMethod"); //Method method1 = clazz.getDeclaredMethod("defaultMethod"); method.invoke(refTest); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } } } --------------------------------------------------------------------------------------- 【CommonTestEntity.java】 public class CommonTestEntity { static { System.out.println("CommonTestEntity執行類加載..."); } public CommonTestEntity() { System.out.println(this.getClass() + " | CommonTestEntity實例初始化 | " + this.getClass().getClassLoader()); } public void defaultMethod() { System.out.println("執行實例方法:defaultMethod"); } }
二、反射調用流程
1.反射的使用
- 1)創建
class
對象(類加載,使用當前方法所在類的ClassLoader
來加載) - 2)獲取
Method
對象(getMethod
和getDeclaredMethod
) - 3)調用
invoke
方法
2.getMethod 和 getDeclaredMethod區別
getMethod源碼如下:
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { Objects.requireNonNull(name); SecurityManager sm = System.getSecurityManager(); if (sm != null) { // 1. 檢查方法權限 checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true); } // 2. 獲取方法 Method method = getMethod0(name, parameterTypes); if (method == null) { throw new NoSuchMethodException(methodToString(name, parameterTypes)); } // 3. 返回方法 return method; } --------------------------------------------------------------------------------------- public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { Objects.requireNonNull(name); SecurityManager sm = System.getSecurityManager(); if (sm != null) { // 1. 檢查方法是權限 checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true); } // 2. 獲取方法 Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes); if (method == null) { throw new NoSuchMethodException(methodToString(name, parameterTypes)); } // 3. 返回方法 return method; }
獲取方法的流程分三步走:
- a.檢查方法權限
- b.獲取方法
Method
對象 - c.返回方法
主要有兩個區別:
1.getMethod
中 checkMemberAccess
傳入的是 Member.PUBLIC
,而 getDeclaredMethod
傳入的是 Member.DECLARED
。
代碼中的註釋:
註釋裡解釋瞭 PUBLIC
和 DECLARED
的不同,PUBLIC
會包括所有的 public
方法,包括父類的方法,而 DECLARED
會包括所有自己定義的方法,public
,protected
,private
都在此,但是不包括父類的方法。
2.getMethod
中獲取方法調用的是 getMethod0
,而 getDeclaredMethod
獲取方法調用的是 privateGetDeclaredMethods
。privateGetDeclaredMethods
是獲取類自身定義的方法,參數是 boolean publicOnly
,表示是否隻獲取公共方法。
privateGetDeclaredMethods 源碼如下:
// Returns an array of "root" methods. These Method objects must NOT // be propagated to the outside world, but must instead be copied // via ReflectionFactory.copyMethod. private Method[] privateGetDeclaredMethods(boolean publicOnly) { checkInitted(); Method[] res; ReflectionData<T> rd = reflectionData(); if (rd != null) { res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods; if (res != null) return res; } // No cached value available; request value from VM res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly)); if (rd != null) { if (publicOnly) { rd.declaredPublicMethods = res; } else { rd.declaredMethods = res; } } return res; }
①relectionData
通過緩存獲取
②如果緩存沒有命中的話,通過 getDeclaredMethods0
獲取方法
getMethod0源碼如下:
private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) { MethodArray interfaceCandidates = new MethodArray(2); Method res = privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates); if (res != null) return res; // Not found on class or superclass directly interfaceCandidates.removeLessSpecifics(); return interfaceCandidates.getFirst(); // may be null }
其中privateGetMethodRecursive
方法中也會調用到privateGetDeclaredMethods
方法和searchMethods
方法
3.getMethod
方法流程
4.getDeclaredMethod
方法流程
三、調用反射方法
invoke源碼:
class Method { public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { Class<?> caller = Reflection.getCallerClass(); // 1. 檢查權限 checkAccess(caller, clazz, Modifier.isStatic(modifiers) ? null : obj.getClass(), modifiers); } // 2. 獲取 MethodAccessor MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { // 創建 MethodAccessor ma = acquireMethodAccessor(); } // 3. 調用 MethodAccessor.invoke return ma.invoke(obj, args); } }
Method.invoke()
實際上並不是自己實現的反射調用邏輯,而是委托給sun.reflect.MethodAccessor
來處理。
每個實際的Java方法隻有一個對應的Method
對象作為root(實質上就是Method類的一個成員變量)。每次在通過反射獲取Method
對象時新創建Method
對象把root
封裝起來。在第一次調用一個實際Java方法對應得Method
對象的invoke()方法之前,實現調用邏輯的MethodAccessor
對象是第一次調用時才會新建並更新給root,然後調用MethodAccessor.invoke()
真正完成反射調用。
MethodAccessor
隻是單方法接口,其invoke()
方法與Method.invoke()
的對應。創建MethodAccessor
實例的是ReflectionFactory
。
MethodAccessor
實現有兩個版本,一個是Java實現的,另一個是native code
實現的。
Java 版本的 MethodAccessorImpl
調用效率比 Native
版本要快 20 倍以上,但是 Java 版本加載時要比 Native 多消耗 3-4 倍資源,所以默認會調用 Native
版本,如果調用次數超過 15 次以後,就會選擇運行效率更高的 Java 版本。
Native版本中的閾值(靜態常量)
四、反射效率低的原因
1.Method#invoke
方法會對參數做封裝和解封操作
我們可以看到,
invoke
方法的參數是Object[]
類型,也就是說,如果方法參數是簡單類型(8中基本數據類型)的話,需要在此轉化成 Object 類型,例如 long ,在javac compile
的時候 用瞭Long.valueOf()
轉型,也就大量瞭生成瞭Long
的 Object, 同時 傳入的參數是Object[]數值,那還需要額外封裝object數組。而在上面
MethodAccessorGenerator#emitInvoke
方法裡我們看到,生成的字節碼時,會把參數數組拆解開來,把參數恢復到沒有被 Object[] 包裝前的樣子,同時還要對參數做校驗,這裡就涉及到瞭解封操作。因此,在反射調用的時候,因為封裝和解封,產生瞭額外的不必要的內存浪費,當調用次數達到一定量的時候,還會導致 GC。
2.需要檢查方法可見性
checkAccess
方法
3.需要遍歷方法並校驗參數
PrivateGetMethodRecursive
中的searhMethod
4.JIT 無法優化
在 JavaDoc 中提到:
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
五、反射優化
1.(網上看到)盡量不要getMethods()
後再遍歷篩選,而直接用getMethod(methodName)
來根據方法名獲取方法
但是在源碼中獲取方法的時候,在searchMethods
方法中,其實也是采用遍歷所有方法的方式。但是相比getMethod,getDeclaredMethod
遍歷的方法數量相對較少,因為不包含父類的方法。
2.緩存class
對象
a)Class.forName
性能比較差
b)如上所述,在獲取具體方法時,每次都要調用native
方法獲取方法列表並遍歷列表,判斷入參類型和返回類型。將反射得到的method/field/constructor
對象做緩存,將極大的提高性能。
3.涉及動態代理的:在實際使用中,CGLIB和Javassist
基於動態代碼的代理實現,性能要優於JDK自帶的動態代理
JDK自帶的動態代理是基於接口的動態代理,相比較直接的反射操作,性能還是高很多,因為接口實例相關元數據在靜態代碼塊中創建並且已經緩存在類成員屬性中,在運行期間是直接調用,沒有額外的反射開銷。
4.使用ReflectASM
,通過生成字節碼的方式加快反射(使用難度大)
到此這篇關於Java反射及性能詳細的文章就介紹到這瞭,更多相關Java反射及性能內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!