大家都說 Java 反射效率低,你知道原因在哪裡么?
- 2019 年 11 月 25 日
- 筆記
預備知識
- 了解 Java 反射基本用法
看完本文可以達到什麼程度
- 了解 Java 反射原理及 Java 反射效率低的原因
文章概覽
summary
我們在 Java 開發中,難免會接觸到反射,而在一些框架中,反射的運用更是常見。我相信,每次提到反射,大家的第一反應一定是反射效率低,盡量少使用。但是反射的效率到底低多少?反射效率低的原因在哪裡?這篇文章就來探索一下這些問題。由於本機上安裝的是 openjdk 12,所以這裡就使用 openjdk 12 源碼進行分析。
我們先看結論,然後分析一下 Java 反射的原理,過程中大家可以根據結論,對源碼做一些思考,然後再根據原理中的一些實現,看看 Java 反射效率低的原因。
零、先放結論
Java 反射效率低主要原因是:
- Method#invoke 方法會對參數做封裝和解封操作
- 需要檢查方法可見性
- 需要校驗參數
- 反射方法難以內聯
- JIT 無法優化
1. 原理–獲取要反射的方法
1.1 反射的使用
我們先來看看 Java 反射使用的一段程式碼:
public class RefTest { public static void main(String[] args) { try { Class clazz = Class.forName("com.zy.java.RefTest"); Object refTest = clazz.newInstance(); Method method = clazz.getDeclaredMethod("refMethod"); method.invoke(refTest); } catch (Exception e) { e.printStackTrace(); } } public void refMethod() { } }
我們在調用反射時,首先會創建 Class 對象,然後獲取其 Method 對象,調用 invoke 方法。獲取反射方法時,有兩個方法,getMethod
和 getDeclaredMethod
,我們就從這兩個方法開始,一步步看下反射的原理。接下來就進入程式碼分析,大家做好準備。
1.2 getMethod / getDeclaredMethod
這裡我們先整體看一下 getMethod 和 getDeclaredMethod 的實現。
class Class { @CallerSensitive 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 getReflectionFactory().copyMethod(method); } @CallerSensitive 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 getReflectionFactory().copyMethod(method); } }
從上面的程式碼,我們可以看到,獲取方法的流程分三步走:
- 檢查方法許可權
- 獲取方法 Method 對象
- 返回方法的拷貝
這裡主要有兩個區別:
- getMethod 中 checkMemberAccess 傳入的是
Member.PUBLIC
,而 getDeclaredMethod 傳入的是Member.DECLARED
這兩個值有什麼區別呢?我們看下程式碼中的注釋:
interface Member { /** * Identifies the set of all public members of a class or interface, * including inherited members. */ public static final int PUBLIC = 0; /** * Identifies the set of declared members of a class or interface. * Inherited members are not included. */ public static final int DECLARED = 1; }
注釋里清楚的解釋了 PUBLIC 和 DECLARED 的不同,PUBLIC 會包括所有的 public 方法,包括父類的方法,而 DECLARED 會包括所有自己定義的方法,public,protected,private 都在此,但是不包括父類的方法。這也正是 getMethod 和 getDeclaredMethod 的區別。
getMethod
中獲取方法調用的是getMethod0
,而getDeclaredMethod
獲取方法調用的是privateGetDeclaredMethods
關於這個區別,這裡簡單提及一下,後面具體分析程式碼。privateGetDeclaredMethods 是獲取類自身定義的方法,參數是 boolean publicOnly,表示是否只獲取公共方法。
private Method[] privateGetDeclaredMethods(boolean publicOnly) { //... }
而 getMethod0 會遞歸查找父類的方法,其中會調用到 privateGetDeclaredMethods 方法。
既然我們上面看了 getMethod 和 getDeclaredMethod 的區別,我們自然選擇 getMethod 方法進行分析,這樣可以走到整個流程。
1.3 getMethod 方法
getMethod 方法流程如下圖:
getMethod
class Class { 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 method = getMethod0(name, parameterTypes); if (method == null) { throw new NoSuchMethodException(methodToString(name, parameterTypes)); } // 3. 返回方法拷貝 return getReflectionFactory().copyMethod(method); } }
我們上面說到獲取方法分三步走:
- 檢查方法許可權
- 獲取方法 Method 對象
- 返回方法的拷貝
我們先看看檢查方法許可權做了些什麼事情。
1.3.1 checkMemberAccess
class Class { private void checkMemberAccess(SecurityManager sm, int which, Class<?> caller, boolean checkProxyInterfaces) { /* Default policy allows access to all {@link Member#PUBLIC} members, * as well as access to classes that have the same class loader as the caller. * In all other cases, it requires RuntimePermission("accessDeclaredMembers") * permission. */ final ClassLoader ccl = ClassLoader.getClassLoader(caller); if (which != Member.PUBLIC) { final ClassLoader cl = getClassLoader0(); if (ccl != cl) { sm.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION); } } this.checkPackageAccess(sm, ccl, checkProxyInterfaces); } }
在這裡可以看到,對於非 Member.PUBLIC 的訪問,會增加一項檢測,SecurityManager.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
這項檢測需要運行時申請 RuntimePermission("accessDeclaredMembers")
。這裡就不繼續往下看了,方法整體是在檢查是否可以訪問對象成員。
接著看下是如何獲取方法的 Method 對象。
1.3.2 getMethod0
class Class { private Method getMethod0(String name, Class<?>[] parameterTypes) { PublicMethods.MethodList res = getMethodsRecursive( name, parameterTypes == null ? EMPTY_CLASS_ARRAY : parameterTypes, /* includeStatic */ true); return res == null ? null : res.getMostSpecific(); } }
這裡是通過 getMethodsRecursive 獲取到 MethodList 對象,然後通過 MethodList#getMostSpecific
方法篩選出對應的方法。MethodList#getMOstSpecific 會篩選返回值類型最為具體的方法,至於為什麼會有返回值的區別,後面會講到。(這裡的具體,指的是有兩個方法,返回值分別是 Child 和 Parent,Child 繼承自 Parent,這裡會篩選出返回值為 Child 的方法)。
接著看 getMethodsRecursive 方法,是如何獲取方法的。
1.3.3 getMethodsRecursive
class Class { private PublicMethods.MethodList getMethodsRecursive(String name, Class<?>[] parameterTypes, boolean includeStatic) { // 1. 獲取自己的 public 方法 Method[] methods = privateGetDeclaredMethods(/* publicOnly */ true); // 2. 篩選符合條件的方法,構造 MethodList 對象 PublicMethods.MethodList res = PublicMethods.MethodList .filter(methods, name, parameterTypes, includeStatic); // 找到方法,直接返回 if (res != null) { return res; } // 3. 沒有找到方法,就獲取其父類,遞歸調用 getMethodsRecursive 方法 Class<?> sc = getSuperclass(); if (sc != null) { res = sc.getMethodsRecursive(name, parameterTypes, includeStatic); } // 4. 獲取介面中對應的方法 for (Class<?> intf : getInterfaces(/* cloneArray */ false)) { res = PublicMethods.MethodList.merge( res, intf.getMethodsRecursive(name, parameterTypes, /* includeStatic */ false)); } return res; } }
這裡獲取方法有四個步驟:
- 通過
privateGetDeclaredMethods
獲取自己所有的 public 方法 - 通過
MethodList#filter
查找 方法名,參數相同的方法,如果找到,直接返回 - 如果自己沒有實現對應的方法,就去父類中查找對應的方法
- 查找介面中對應的方法
通過上面四個步驟,最終獲取到的是一個 MethodList
對象,是一個鏈表結點,其 next
指向下一個結點。也就是說,這裡獲取到的 Method 會有多個。這裡稍微解釋一下,在我們平時編寫 Java 程式碼時,同一個類是不能有方法名和方法參數都相同的方法的,而實際上,在 JVM 中,一個方法簽名是和 返回值,方法名,方法參數 三者相關的。也就是說,在 JVM 中,可以存在 方法名和方法參數都相同,但是返回值不同的方法。所以這裡返回的是一個方法鏈表。所以上面最終返回方法時會通過 MethodList#getMostSpecific
進行返回值的篩選,篩選出返回值類型最具體的方法。
這裡我們先暫停回顧一下整體的調用鏈路:
getMethod -> getMethod0 -> getMethodsRecursive -> privateGetDeclaredMethods
通過函數調用,最終會調用到 privateGetDeclaredMethods
方法,也就是真正獲取方法的地方。
1.3.4 privateGetDeclaredMethods
class Class { private Method[] privateGetDeclaredMethods(boolean publicOnly) { Method[] res; // 1. 通過快取獲取 Method[] ReflectionData<T> rd = reflectionData(); if (rd != null) { res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods; if (res != null) return res; } // 2. 沒有快取,通過 JVM 獲取 res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly)); if (rd != null) { if (publicOnly) { rd.declaredPublicMethods = res; } else { rd.declaredMethods = res; } } return res; } }
在 privateGetDeclaredMethods 獲取方法時,有兩個步驟:
- relectionData 通過快取獲取
- 如果快取沒有命中的話,通過 getDeclaredMethods0 獲取方法
先看看 relectionData 方法:
class Class { private ReflectionData<T> reflectionData() { SoftReference<ReflectionData<T>> reflectionData = this.reflectionData; int classRedefinedCount = this.classRedefinedCount; ReflectionData<T> rd; if (reflectionData != null && (rd = reflectionData.get()) != null && rd.redefinedCount == classRedefinedCount) { return rd; } // else no SoftReference or cleared SoftReference or stale ReflectionData // -> create and replace new instance return newReflectionData(reflectionData, classRedefinedCount); } }
在 Class 中會維護一個 ReflectionData 的軟引用,作為反射數據的快取。ReflectionData 結構如下:
private static class ReflectionData<T> { volatile Field[] declaredFields; volatile Field[] publicFields; volatile Method[] declaredMethods; volatile Method[] publicMethods; volatile Constructor<T>[] declaredConstructors; volatile Constructor<T>[] publicConstructors; // Intermediate results for getFields and getMethods volatile Field[] declaredPublicFields; volatile Method[] declaredPublicMethods; volatile Class<?>[] interfaces; // Cached names String simpleName; String canonicalName; static final String NULL_SENTINEL = new String(); // Value of classRedefinedCount when we created this ReflectionData instance final int redefinedCount; }
可以看到,保存了 Class 中的屬性和方法。如果快取為空,就會通過 getDeclaredMethods0 從 JVM 中查找方法。getDeclaredMethods0 是一個 native 方法,這裡暫時先不看。
通過上面幾個步驟,就獲取到 Method 數組。
這就是 getMethod 方法的整個實現了。我們再回過頭看一下 getDeclaredMethod 方法的實現,通過 privateGetDeclaredMethods 獲取方法以後,會通過 searchMethods 對方法進行篩選。
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { // ... Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes); // ... }
searchMethods 方法實現比較簡單,就是對比方法名,參數,方法返回值。
class Class { private static Method searchMethods(Method[] methods, String name, Class<?>[] parameterTypes) { ReflectionFactory fact = getReflectionFactory(); Method res = null; for (Method m : methods) { // 比較方法名 if (m.getName().equals(name) // 比較方法參數 && arrayContentsEq(parameterTypes, fact.getExecutableSharedParameterTypes(m)) // 比較返回值 && (res == null || (res.getReturnType() != m.getReturnType() && res.getReturnType().isAssignableFrom(m.getReturnType())))) res = m; } return res; } }
1.3.5 Method#copy
在獲取到對應方法以後,並不會直接返回,而是會通過 getReflectionFactory().copyMethod(method); 返回方法的一個拷貝。最終調用的是 Method#copy,我們來看看其實現。
class Method { Method copy() { // This routine enables sharing of MethodAccessor objects // among Method objects which refer to the same underlying // method in the VM. (All of this contortion is only necessary // because of the "accessibility" bit in AccessibleObject, // which implicitly requires that new java.lang.reflect // objects be fabricated for each reflective call on Class // objects.) if (this.root != null) throw new IllegalArgumentException("Can not copy a non-root Method"); Method res = new Method(clazz, name, parameterTypes, returnType, exceptionTypes, modifiers, slot, signature, annotations, parameterAnnotations, annotationDefault); res.root = this; // Might as well eagerly propagate this if already present res.methodAccessor = methodAccessor; return res; } }
會 new 一個 Method 實例並返回。這裡有兩點要注意:
- 設置 root = this
- 會給 Method 設置 MethodAccessor,用於後面方法調用。也就是所有的 Method 的拷貝都會使用同一份 methodAccessor。
通過上面的步驟,就獲取到了需要反射的方法。我們再回顧一下之前的流程。
getMethod
2. 原理–調用反射方法
獲取到方法以後,通過 Method#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); } }
invoke 方法的實現,分為三步:
2.1 檢查是否有許可權調用方法
這裡對 override 變數進行判斷,如果 override == true,就跳過檢查 我們通常在 Method#invoke 之前,會調用 Method#setAccessible(true),就是設置 override 值為 true。
2.2 獲取 MethodAccessor
在上面獲取 Method 的時候我們講到過,Method#copy 會給 Method 的 methodAccessor 賦值。所以這裡的 methodAccessor 就是拷貝時使用的 MethodAccessor。如果 ma 為空,就去創建 MethodAccessor。
class Method { private MethodAccessor acquireMethodAccessor() { // First check to see if one has been created yet, and take it // if so MethodAccessor tmp = null; if (root != null) tmp = root.getMethodAccessor(); if (tmp != null) { methodAccessor = tmp; } else { // Otherwise fabricate one and propagate it up to the root tmp = reflectionFactory.newMethodAccessor(this); setMethodAccessor(tmp); } return tmp; } }
這裡會先查找 root 的 MethodAccessor,這裡的 root 在上面 Method#copy 中設置過。如果還是沒有找到,就去創建 MethodAccessor。
class ReflectionFactory { public MethodAccessor newMethodAccessor(Method method) { // 其中會對 noInflation 進行賦值 checkInitted(); // ... if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { // 生成的是 MethodAccessorImpl return new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); } else { NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method); DelegatingMethodAccessorImpl res = new DelegatingMethodAccessorImpl(acc); acc.setParent(res); return res; } } }
這裡可以看到,一共有三種 MethodAccessor。MethodAccessorImpl
,NativeMethodAccessorImpl
,DelegatingMethodAccessorImpl
。採用哪種 MethodAccessor 根據 noInflation
進行判斷,noInflation 默認值為 false,只有指定了 sun.reflect.noInflation 屬性為 true,才會 採用 MethodAccessorImpl。所以默認會調用 NativeMethodAccessorImpl。
MethodAccessorImpl 是通過動態生成位元組碼來進行方法調用的,是 Java 版本的 MethodAccessor,位元組碼生成比較複雜,這裡不放程式碼了。大家感興趣可以看這裡的 generate 方法。
DelegatingMethodAccessorImpl 就是單純的代理,真正的實現還是 NativeMethodAccessorImpl。
class DelegatingMethodAccessorImpl extends MethodAccessorImpl { private MethodAccessorImpl delegate; DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) { setDelegate(delegate); } public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { return delegate.invoke(obj, args); } void setDelegate(MethodAccessorImpl delegate) { this.delegate = delegate; } }
NativeMethodAccessorImpl 是 Native 版本的 MethodAccessor 實現。
class NativeMethodAccessorImpl extends MethodAccessorImpl { public Object invoke(Object obj, Object[] args) throws IllegalArgumentException, InvocationTargetException { // We can't inflate methods belonging to vm-anonymous classes because // that kind of class can't be referred to by name, hence can't be // found from the generated bytecode. if (++numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { // Java 版本的 MethodAccessor MethodAccessorImpl acc = (MethodAccessorImpl) new MethodAccessorGenerator(). generateMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes(), method.getReturnType(), method.getExceptionTypes(), method.getModifiers()); parent.setDelegate(acc); } // Native 版本調用 return invoke0(method, obj, args); } private static native Object invoke0(Method m, Object obj, Object[] args); }
在 NativeMethodAccessorImpl 的實現中,我們可以看到,有一個 numInvocations 閥值控制,numInvocations 表示調用次數。如果 numInvocations 大於 15(默認閥值是 15),那麼就使用 Java 版本的 MethodAccessorImpl。
為什麼採用這個策略呢,可以 JDK 中的注釋:
// "Inflation" mechanism. Loading bytecodes to implement // Method.invoke() and Constructor.newInstance() currently costs // 3-4x more than an invocation via native code for the first // invocation (though subsequent invocations have been benchmarked // to be over 20x faster). Unfortunately this cost increases // startup time for certain applications that use reflection // intensively (but only once per class) to bootstrap themselves. // To avoid this penalty we reuse the existing JVM entry points // for the first few invocations of Methods and Constructors and // then switch to the bytecode-based implementations. // // Package-private to be accessible to NativeMethodAccessorImpl // and NativeConstructorAccessorImpl private static boolean noInflation = false;
Java 版本的 MethodAccessorImpl 調用效率比 Native 版本要快 20 倍以上,但是 Java 版本載入時要比 Native 多消耗 3-4 倍資源,所以默認會調用 Native 版本,如果調用次數超過 15 次以後,就會選擇運行效率更高的 Java 版本。那為什麼 Native 版本運行效率會沒有 Java 版本高呢?從 R 大部落格來看,是因為 這是HotSpot的優化方式帶來的性能特性,同時也是許多虛擬機的共同點:跨越native邊界會對優化有阻礙作用,它就像個黑箱一樣讓虛擬機難以分析也將其內聯,於是運行時間長了之後反而是託管版本的程式碼更快些。
2.3 調用 MethodAccessor#invoke 實現方法的調用
在生成 MethodAccessor 以後,就調用其 invoke 方法進行最終的反射調用。這裡我們對 Java 版本的 MethodAccessorImpl 做個簡單的分析,Native 版本暫時不做分析。在前面我們提到過 MethodAccessorImpl 是通過 MethodAccessorGenerator#generate 生成動態位元組碼然後動態載入到 JVM 中的。其中生成 invoke 方法位元組碼的是 MethodAccessorGenerator#emitInvoke。我們看其中校驗參數的一小段程式碼:
// Iterate through incoming actual parameters, ensuring that each // is compatible with the formal parameter type, and pushing the // actual on the operand stack (unboxing and widening if necessary). // num args of other invoke bytecodes for (int i = 0; i < parameterTypes.length; i++) { // ... if (isPrimitive(paramType)) { // Unboxing code. // Put parameter into temporary local variable // astore_3 | astore_2 // ... // repeat for all possible widening conversions: // aload_3 | aload_2 // instanceof <primitive boxing type> // ifeq <next unboxing label> // aload_3 | aload_2 // checkcast <primitive boxing type> // Note: this is "redundant", // // but necessary for the verifier // invokevirtual <unboxing method> // <widening conversion bytecode, if necessary> // goto <next parameter label> // <next unboxing label:> ... // last unboxing label: // new <IllegalArgumentException> // dup // invokespecial <IllegalArgumentException ctor> // athrow } }
通過上面的注釋以及位元組碼,我們可以看到,生成的 invoke 方法,會對傳入的參數做校驗,其中會涉及到 unboxing 操作。
到此,基本上 Java 方法反射的原理就介紹完了。
3. Java 反射效率低的原因
了解了反射的原理以後,我們來分析一下反射效率低的原因。
1. Method#invoke 方法會對參數做封裝和解封操作
我們可以看到,invoke 方法的參數是 Object[] 類型,也就是說,如果方法參數是簡單類型的話,需要在此轉化成 Object 類型,例如 long ,在 javac compile 的時候 用了Long.valueOf() 轉型,也就大量了生成了Long 的 Object, 同時 傳入的參數是Object[]數值,那還需要額外封裝object數組。而在上面 MethodAccessorGenerator#emitInvoke 方法里我們看到,生成的位元組碼時,會把參數數組拆解開來,把參數恢復到沒有被 Object[] 包裝前的樣子,同時還要對參數做校驗,這裡就涉及到了解封操作。因此,在反射調用的時候,因為封裝和解封,產生了額外的不必要的記憶體浪費,當調用次數達到一定量的時候,還會導致 GC。
2. 需要檢查方法可見性
通過上面的源碼分析,我們會發現,反射時每次調用都必須檢查方法的可見性(在 Method.invoke 里)
3. 需要校驗參數
反射時也必須檢查每個實際參數與形式參數的類型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);
4. 反射方法難以內聯
Method#invoke 就像是個獨木橋一樣,各處的反射調用都要擠過去,在調用點上收集到的類型資訊就會很亂,影響內聯程式的判斷,使得 Method.invoke() 自身難以被內聯到調用方。參見 www.iteye.com/blog/rednax…
5. 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.
因為反射涉及到動態載入的類型,所以無法進行優化。
總結
上面就是對反射原理和反射效率低的一些分析。