1. 程式人生 > >淺談Java反射的實現原理

淺談Java反射的實現原理

從一段示例程式碼開始

        Class clz = Class.forName("ClassA");
        Object instance = clz.newInstance();
        Method method = clz.getMethod("myMethod", String.class);
        method.invoke(instance, "abc","efg");

前兩行實現了類的裝載、連結和初始化(newInstance方法實際上也是使用反射呼叫了<init>方法),後兩行實現了從class物件中獲取到method物件然後執行反射呼叫。試想一下,如果Method.invoke

方法內,動態拼接成如下程式碼,轉化成JVM能執行的位元組碼,就可以實現反射呼叫了。

     public Object invoke(Object obj, Object[] param){
        MyClass instance=(MyClass)obj;
        return instance.myMethod(param[0],param[1],...);
     }

Class和Method物件

Class物件裡維護著該類的所有Method,Field,Constructor的cache,這份cache也可以被稱作根物件。每次getMethod獲取到的Method物件都持有對根物件的引用,因為一些重量級的Method的成員變數(主要是MethodAccessor),我們不希望每次建立Method物件都要重新初始化,於是所有代表同一個方法的Method物件都共享著根物件的MethodAccessor,每一次建立都會呼叫根物件的copy方法複製一份:

    Method copy() { 
        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        res.root = this;
        res.methodAccessor = methodAccessor;
        return
res; }

反射呼叫

    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

呼叫Method.invoke之後,先進行訪問許可權檢查,再獲取MethodAccessor物件,並呼叫MethodAccessor.invoke方法。MethodAccessor被同名Method物件所共享,由ReflectionFactory建立。建立機制採用了一種名為inflation的方式(JDK1.4之後):如果該方法的累計呼叫次數<=15,會創建出NativeMethodAccessorImpl,它的實現就是直接呼叫native方法實現反射;如果該方法的累計呼叫次數>15,會創建出由位元組碼組裝而成的MethodAccessorImpl。(是否採用inflation和15這個數字都可以在jvm引數中調整)

那麼以示例的反射呼叫ClassA.myMethod(String,String)為例,生成MethodAccessorImpl類的位元組碼對應成Java程式碼如下:

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {    
    public Object invoke(Object obj, Object[] args)  throws Exception {
        try {
            MyClass target = (ClassA) obj;
            String arg0 = (String) args[0];
            String arg1 = (String) args[1];
            target.myMethod(arg0,arg1);
        } catch (Throwable t) {
            throw new InvocationTargetException(t);
        }
    }
}

效能

通過JNI呼叫native方法初始化更快,但對優化有阻礙作用。隨著呼叫次數的增多,使用拼裝出的位元組碼可以直接以Java呼叫的方式來實現反射,發揮了JIT的優化作用。

那麼為什麼Java反射呼叫被普通的方法呼叫慢很多呢?我認為主要有以下三點原因:

  1. 因為介面的通用性,Java的invoke方法是傳object和object[]陣列的。基本型別引數需要裝箱和拆箱,產生大量額外的物件和記憶體開銷,頻繁促發GC。
  2. 編譯器難以對動態呼叫的程式碼提前做優化,比如方法內聯。
  3. 反射需要按名檢索類和方法,有一定的時間開銷。

參考