JVMの反射呼び出し実装

8157 ワード

反射呼び出しの実行方法
Javaの反射呼び出しはjava.lang.reflect.Methodのinvokeは実行を呼び出し、Methodインスタンスは、実行されたメソッドのクラスのClassインスタンスが提供するメソッドを反射することによって取得される.
package com.test;
import java.lang.reflect.Method;
public class ReflectTest {
    public void method1(int arg) {
        new RuntimeException("xxxxxxxx").printStackTrace();;
    }
    public static void main(String[] args) throws Exception {
                //            Method   
        Method method = ReflectTest.class.getMethod("method1", int.class); 
                //  method invoke        ,              ,         (         )
        method.invoke(new ReflectTest(), 128);
    }
}
  :
java.lang.RuntimeException: xxxxxxxx
    at com.test.ReflectTest.method1(ReflectTest.java:8)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.test.ReflectTest.main(ReflectTest.java:13)

反射呼び出しの異常呼び出しスタックから、Methodインスタンスの反射実行がsunを通過することが分かる.reflect.NativeMethodAccessorImplの呼び出しですが、これは一般的なケースであり、以下ではMethod反射による実装原理を具体的に分析します.
Method反射呼び出しの原理
調べてinvoke(jdk 1.8.0)のソースコード、
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は、native codeによって反射呼び出しが実現され、java自体が使用される2つの既存の具体的な実装があるインタフェースです.実際のJavaメソッドのMethodオブジェクトに対するinvoke()メソッドを初めて呼び出す前に、呼び出しロジックを実現するMethodAccessorオブジェクトはまだ作成されていません.MethodAccessorを新規作成しrootに更新してからMethodAccessorを呼び出す.invoke()は本当に反射呼び出しを完了します.委任されたMethodAccessorクラス作成プロセスのキーコードは次のとおりです.
if (noInflation) {  
            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;  
}

NoInflationフラグビットがfalseの場合、DelegatingMethodAccessorImplインプリメンテーションクラスが作成され、DelegatingMethodAccessorImplはNativeMethodAccessorImpl(native codeインプリメンテーションのMethodAccessor)を代理します.DelegatingMethodAccessorImpl:
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;  
    }  
}

2つのバージョンの性能を比較するために、SunのJDKは「inflation」のテクニックを使用しています.Javaメソッドを反射呼び出し時に、最初にnative版を何回か使用させ、反射呼び出し回数がしきい値を超えると専用のMethodAccessor実装クラスを生成し、その中のinvoke()メソッドのバイトコードを生成し、後でこのJavaメソッドの反射呼び出しにJava版を使用します.MethodAccessorを切り替える実装クラスのバルブ値パラメータを-Dsunとする.reflect.inflationThreshold .反射呼び出しのInflationメカニズムは、パラメータ(−DSun.reflect.noInflation=true)によってオフにすることができる.これにより,反射呼び出しの最初からJava実装のMethodAccessorが直接生成される.JavaバージョンのMethodAccessor実装は、概ね次のようになります(先頭のcom.test.ReflectTestクラスバージョンに基づいています).
package sun.reflect;  
  
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {      
    public GeneratedMethodAccessor1() {  
        super();  
    }  
      
    public Object invoke(Object obj, Object[] args)     
        throws IllegalArgumentException, InvocationTargetException {  
        // prepare the target and parameters  
        if (obj == null) throw new NullPointerException();  
        try {  
            com.test.ReflectTest target = (com.test.ReflectTest) obj;  
            if (args.length != 1) throw new IllegalArgumentException();  
            int arg0 = (int) args[0];  
        } catch (ClassCastException e) {  
            throw new IllegalArgumentException(e.toString());  
        } catch (NullPointerException e) {  
            throw new IllegalArgumentException(e.toString());  
        }  
        // make the invocation  
        try {  
            target.method1(arg0);  
        } catch (Throwable t) {  
            throw new InvocationTargetException(t);  
        }  
    }  
}  

JavaバージョンのMethodAccessor実装が表示され、その反射呼び出しはターゲットメソッドのinvokevirtual呼び出しとみなすことができる.この反射呼び出しがホットスポットとなると、Methodに近い位置に内蔵することもできる.invoke()の側は,反射呼び出しのオーバーヘッドを大幅に低減した.一方、native版の反射呼び出しは有効に接続できないため、呼び出しオーバーヘッドはプログラムの実行に伴って低減できない.
2つの実装のパフォーマンス比較
  • Java実装のバージョンは初期化に時間がかかりますが、実行性能は
  • が優れています.
  • nativeバージョンは正反対で、起動時は比較的速い
  • nativeバージョンでは、JVMの最適化が阻害されます(native境界を越えると最適化が阻害され、ブラックボックスのように仮想マシンが分析しにくくなり、内蔵されます).
  • パフォーマンスの最適化
    JavaバージョンのMethodAccessorで実装される反射呼び出しのパフォーマンスが最適化されるのは、主にインスタントコンパイラのメソッドがインラインされているためです.Inflationをオフにした場合、インラインのボトルネックはMethodにある.invokeメソッドではMethodAccessor.invokeメソッドの呼び出し.Java仮想マシンの上記呼び出しポイントに関するタイプprofile(注:invokevirtualまたはinvokeinterfaceの場合、Java仮想機会は呼び出し者の具体的なタイプを記録し、タイプprofileと呼ぶ)は、このような複数のクラスを同時に記録することができないため、測定された反射呼び出しがインラインされない場合がある.次に、呼び出しポイントのタイプprofileがバルブ値を超えているため、JVMがホットスポット反射呼び出しをインライン最適化できない性能損失を比較します.まず、インライン最適化が可能な場合を見てみましょう.
    public static void main(String[] args) throws Exception {
                    Method method = ReflectTest.class.getMethod("method1", int.class);
    //      polluteProfile();
            ReflectTest obj = new ReflectTest();
            long cur = System.currentTimeMillis();
            for(int i=0;i<2000000000;i++) {
                if(i % 100000000 == 0) {
                    long tmp = System.currentTimeMillis();
                    System.out.println("cost=" + (tmp - cur));
                    cur = tmp;
            }
            method.invoke(obj, 128);    
    }
              :
    cost=980
    cost=1150
    cost=1132
    cost=1341
    cost=1417       
    

    以下は、反射されたクラスの複数のメソッドのMethodオブジェクトが共存するシーンを作成することによって、JVMのホットスポットインライン最適化を乱すことです.
    //         
        private static void polluteProfile() throws Exception {
            Method method1 = ReflectTest.class.getMethod("method2", int.class);
            Method method2 = ReflectTest.class.getMethod("method3", int.class);
            ReflectTest obj = new ReflectTest();
            for(int i=0;i<2000;i++) {
                method1.invoke(obj, 1);
                method1.invoke(obj, 2);
            }
        }
        
        public static void main(String[] args) throws Exception {
            Method method = ReflectTest.class.getMethod("method1", int.class);
            polluteProfile();
            ReflectTest obj = new ReflectTest();
            long cur = System.currentTimeMillis();
            for(int i=0;i<2000000000;i++) {
                if(i % 100000000 == 0) {
                    long tmp = System.currentTimeMillis();
                    System.out.println("cost=" + (tmp - cur));
                    cur = tmp;
                }
                method.invoke(obj, 128);    
            }
          }
              :
    cost=5545
    cost=5521
    cost=5488
    cost=5536
    cost=5488
    

    Java仮想マシンの各呼び出しについて記録できるタイプ数を増やすことができます(仮想マシンパラメータ-XX:TypeProfileWidthに対応し、デフォルト値は2、実測-XX:TypeProfileWidth=3はJVMパラメータとしてローカルノートパソコンでは無効です).
    リファレンス
  • 反射呼び出し方法に関するlog