リフレクションを介したメソッドの呼び出し

すべてのJavaプログラマは、明示的または暗黙的にリフレクションを使用してメソッドを呼び出します。 自分でやらなかったとしても、使用するライブラリまたはフレームワークはおそらくあなたのためにそれをしているでしょう。 この呼び出しが内部でどのように配置され、どのくらい高速であるかを見てみましょう。 OpenJDK 8で最新の更新を確認します。







Method.invokeメソッド自体で調査を開始する必要があります 。 そこで3つのことが行われます。









アクセス制御は2つの部分で構成されています。 簡単なチェックにより、メソッドとそれを含むクラスの両方にpublic



修飾子があることが確認されます。 そうでない場合は、呼び出し元のクラスがこのメソッドにアクセスできることを確認します。 呼び出し元のクラスを見つけるには、プライベートメソッドReflection.getCallerClass()



ます。 ちなみに、一部の人々は自分のコードでそれを使用するのが好きです。 Java 9では、公開のStack-Walking APIが表示され、それに切り替えることは非常に合理的です。







事前にmethod.setAccessible(true)



呼び出すことにより、アクセスチェックをキャンセルできることが知られています。 このセッターは、チェックを無視するようにoverride



フラグを設定しoverride



。 メソッドがパブリックであることを知っていても、 setAccessible(true)



を設定すると、検証にかかる時間が少し節約されます。







いくつの異なるシナリオが時間を浪費するかを見てみましょう。 パブリックメソッドと非パブリックメソッドを持つ単純なクラスを作成しましょう。







 public static class Person { private String name; Person(String name) { this.name = name; } public String getName1() { return name; } protected String getName2() { return name; } }
      
      





2つのフラグ( accessible



およびnonpublic



パラメーター化されたJMHテストを記述します。 これが準備になります。







 Method method; Person p; @Setup public void setup() throws Exception { method = Person.class.getDeclaredMethod(nonpublic ? "getName2" : "getName1"); method.setAccessible(accessible); p = new Person(String.valueOf(Math.random())); }
      
      





そして、ベンチマーク自体:







 @Benchmark public String reflect() throws Exception { return (String) method.invoke(p); }
      
      





私はこれらの結果を見ます(3つのフォーク、5x500msのウォームアップ、10x500msの測定):







(アクセス可能) (非公開) 時間
本当 本当 5.062±0.056 ns / op
本当 5.042±0.032 ns / op
本当 6.078±0.039 ns / op
5.835±0.028 ns / op


実際、 setAccessible(true)



実行されると、最速になります。 この場合、メソッドがパブリックであるかどうかに違いはありません。 setAccessible(false)



場合、両方のテストが遅くなり、非パブリックメソッドはパブリックメソッドよりも若干遅くなります。 しかし、その差はより大きくなると予想していました。 ここでは、 Reflection.getCallerClass()



がJITコンパイラの組み込み関数であることが主に役立ちます。ほとんどの場合コンパイル中に定数置き換えられます。 getCallerClass()



が返されることを意味し、知っています。 さらに、チェックは本質的には、呼び出されたクラスと呼び出し元クラスのパッケージを比較することになります。 パッケージが異なる場合、クラス階層はまだチェックされます。







次に何が起こりますか? 次に、 MethodAccessor



オブジェクトを作成する必要があります。 ところで、 Person.class.getMethod("getName")



は常にMethod



オブジェクトの新しいインスタンスを返すという事実にもかかわらず、 Person.class.getMethod("getName")



内で使用されるMethodAccessor



ルートフィールドを介して再利用されます。 ただし、 getMethod



自体getMethod



呼び出しよりも大幅に遅いため、メソッドを複数回呼び出す予定がある場合は、 Method



オブジェクトを保存することをお勧めします。







MethodAccessor



ReflectionFactoryによって作成されます。 ここに、グローバルJVM設定によって制御される2つのシナリオがあります。









-Dsun.reflect.noInflation=true



を有効にし、JNIのみを使用する場合(このために大きなしきい値-Dsun.reflect.inflationThreshold=100000000



を設定する)、テストで何が起こるかを見てみましょう。







(アクセス可能) (非公開) デフォルト インフレ率 JNIのみ
本当 本当 5,062±0,056 4.935±0.375 195,960±1,873
本当 5.042±0.032 4.914±0.329 194,722±1,151
本当 6.078±0.039 5.638±0.050 196.196±0.910
5.835±0.028 5.520±0.042 194.626±0.918


以降、すべての結果は操作あたりナノ秒になります。 予想どおり、JNIは大幅に遅いため、このようなモードをオンにすることは不当です。 不思議なことに、noInflationモードは少し高速でした。 これは、 DelegatingMethodAccessorImpl



がないため、1つの間接アドレス指定の必要性がなくなります。 デフォルトでは、呼び出しはMethod → DelegatingMethodAccessorImpl → GeneratedMethodAccessorXYZ



を経由し、このオプションを使用すると、チェーンはMethod → GeneratedMethodAccessorXYZ



短縮されます。 Method → DelegatingMethodAccessorImpl



の呼び出しMethod → DelegatingMethodAccessorImpl



単相であり、仮想化するのは簡単ですが、間接的なアドレス指定は依然として残っています。







仮想化といえば。 実際のプログラムでは状況が表示されないため、ベンチマークが悪いことに注意してください。 ベンチマークでは、リフレクションを介して1つのメソッドのみを呼び出します。つまり、生成されたアクセサーは1つだけであり、これも簡単に仮想化され、インラインです。 実際のアプリケーションでは、これは起こりません。多くのアクセサーがあります。 この状況をシミュレートするために、 setup



メソッドでオプションでタイププロファイルをポイズニングしましょう。







 if(polymorph) { Method method2 = Person.class.getMethod("toString"); Method method3 = Person.class.getMethod("hashCode"); for(int i=0; i<3000; i++) { method2.invoke(p); method3.invoke(p); } }
      
      





パフォーマンスを測定するコードを変更しなかったことに注意してください。 この前に、一見役に立たない数千の呼び出しを行いました。 ただし、これらの無用な呼び出しは少し画像を台無しにします。JITは多くのオプションがあることを認識しており、唯一の可能なオプションを代用することはできず、今では正直な仮想呼び出しを行っています。 結果は次のようになります(poly-メソッド呼び出しをポリモーフィックに変換するオプションは、JNIに影響しません):







(acc) (非公開) デフォルト デフォルト/ポリ インフレ率 インフレ/ポリ JNIのみ
本当 本当 5,062±0,056 6.848±0.031 4.935±0.375 6.509±0.032 195,960±1,873
本当 5.042±0.032 6.847±0.035 4.914±0.329 6.490±0.037 194,722±1,151
本当 6.078±0.039 7.855±0.040 5.638±0.050 7.661±0.049 196.196±0.910
5.835±0.028 7.568±0.046 5.520±0.042 7.111±0.058 194.626±0.918


ご覧のように、仮想コールにより、ハードウェアに約1.5〜1.8 nsが追加されます。これは、アクセスチェック以上のものです。 マイクロベンチマーク内の仮想マシンの動作は、実際のアプリケーションの動作とは大幅に異なる可能性があり、可能であれば現実に近い状態を再現できることに留意することが重要です。 もちろん、ここではすべてが現実からはほど遠いものです。少なくとも、ガーベッジがないため、プロセッサーのL1キャッシュとガーベッジコレクションに必要なオブジェクトはすべて発生しません。







-Dsun.reflect.noInflation=true



物事が速くなると言うクールな人もいます。 わずか0.3 nsと仮定しますが、それでもです。 はい、さらに最初の15コールが加速します。 はい、そしてワーキングセットがわずかに減少しました。プロセッサキャッシュを保存します-確実なプラス! プロダクションにオプションを追加して修復します! これは必要ありません。 ベンチマークでは、1つのシナリオをテストしましたが、実際には他のシナリオがあります。 たとえば、一部のコードは多くの異なるメソッドを1回呼び出すことがあります。 このオプションを使用すると、最初の呼び出しですぐにアクセサーが生成されます。 費用はいくらですか? アクセサーはどのくらい生成しますか?







これを評価するために、リフレクションを通じてプライベートフィールドMethod.methodAccessor



(以前にMethod.root



をクリアした)をクリアし、アクセサの初期化を再度強制することができます。 リフレクションによるフィールドの記録は最適化されているため、テストの速度はそれほど低下しません。 そのような結果が得られます。 一番上の行-以前に取得した結果(多態性、アクセス可能)、比較用:







(テスト) デフォルト インフレ率 ジニ
呼び出す 6.848±0.031 6.509±0.032 195,960±1,873
リセット+呼び出し 227.133±9.159 100,195.746±2060.810 236.900±2.042


ご覧のとおり、アクセサがリセットされると、デフォルトでは、JNIを使​​用したバージョンよりもパフォーマンスがわずかに低下します。 しかし、JNIを完全に拒否すると、メソッドを開始するのに100マイクロ秒かかります。 単一のメソッド呼び出し(JNI経由でも)と比較して、実行時にクラスを生成およびロードすることは、もちろん非常に遅くなります。 したがって、デフォルトの動作「JNIを15回試行してからクラスを生成する」は非常に合理的です。







一般に、アプリケーションを高速化する魔法のオプションはないことを忘れないでください。 存在する場合、デフォルトで有効になります。 人々からそれを隠すことのポイントは何ですか? アプリケーションを特に高速化するオプションがあるかもしれませんが、「-XX:+ MakeJavaFasterをハックすれば、すべてが飛ぶ」などのアドバイスを信じないでください。







これらの生成されたアクセサーはどのように見えますか? バイトコードは、かなり単純な低レベルAPI ClassFileAssemblerを使用してMethodAccessorGeneratorクラスで生成されます。これは、削除されたASMライブラリーにいくらか似ています。 クラスに sun.reflect.GeneratedMethodAccessorXYZ



という形式の名前が付けられます 。ここで、 XYZ



はグローバル同期カウンターであり、スタックトレースとデバッガーで確認できます。







生成されたクラスはメモリ内にのみ存在しますが、 ClassDefiner.defineClassメソッドに型の行を追加することで簡単にディスクにダンプできます







 try { Files.write(Paths.get(name+".class"), bytes); } catch(Exception ex) {}
      
      





その後、逆コンパイラでクラスを見ることができます。 getName1()



メソッドの場合、次のコードが生成されました(FernFlowerデコンパイラーおよび変数の手動の名前変更):







 public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() {} public Object invoke(Object target, Object[] args) throws InvocationTargetException { if(target == null) { throw new NullPointerException(); } else { Person person; try { person = (Person)target; if(args != null && args.length != 0) { throw new IllegalArgumentException(); } } catch (NullPointerException | ClassCastException ex) { throw new IllegalArgumentException(ex.toString()); } try { return person.getName1(); } catch (Throwable ex) { throw new InvocationTargetException(ex); } } } }
      
      





あなたがしなければならない余分な事に注意してください。 必要な型の空でないオブジェクトが与えられ、引数のリストの代わりに空の引数のリストまたはnull



が渡されたことを確認する必要があります(誰もが知っているわけではありませんが、引数なしでリフレクションメソッドを呼び出す場合、空の配列の代わりにnull



を渡すことができnull



)。 同時に、コントラクトを注意深く観察する必要があります。オブジェクトの代わりにnull



渡された場合、 NullPointerException



スローします。 別のクラスのオブジェクトを渡した場合、 IllegalArgumentException



person.getName1()



実行中に例外が発生した場合、 InvocationTargetException



が発生します。 そして、このメソッドには引数がありません。 そして、もしそうなら? たとえば、このようなメソッドを呼び出します(変更の場合、現在は静的であり、 void



を返しvoid



)。







 class Test { public static void test(String s, int x) {} }
      
      





現在、大幅に多くのコードがあります。







 public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public GeneratedMethodAccessor1() {} public Object invoke(Object target, Object[] args) throws InvocationTargetException { String s; int x; try { if(args.length != 2) { throw new IllegalArgumentException(); } s = (String)args[0]; Object arg = args[1]; if(arg instanceof Byte) { x = ((Byte)arg).byteValue(); } else if(arg instanceof Character) { x = ((Character)arg).charValue(); } else if(arg instanceof Short) { x = ((Short)arg).shortValue(); } else { if(!(arg instanceof Integer)) { throw new IllegalArgumentException(); } x = ((Integer)arg).intValue(); } } catch (NullPointerException | ClassCastException ex) { throw new IllegalArgumentException(ex.toString()); } try { Test.test(s, x); return null; } catch (Throwable ex) { throw new InvocationTargetException(ex); } } }
      
      





int



代わりに、 Byte



Short



Character



またはInteger



を渡す権利があり、これらはすべて変換する必要があることに注意してください。 これは、変換が行われている場所です。 このようなブロックは、拡張変換が可能な各プリミティブ引数ごとに追加されます。 また、キャッチがNullPointerException



によってキャッチされる理由も明らかです。これは、アンボックス中に発生する可能性があり、その後IllegalArgumentException



IllegalArgumentException



必要があります。 しかし、メソッドは静的であるという事実のため、 target



パラメーターの内容は気にしません。 さて、このメソッドはvoid



返すため、 return null



行がありました。 このすべての魔法は、 MethodAccessorGenerator.emitInvokeにきちんと描かれています







これがメソッド呼び出しの仕組みです。 コンストラクターの呼び出しも同様に配置されます。 また、このコードはオブジェクトの逆シリアル化のために部分的に再利用されます。 JVMの観点からアクセサーが既に存在する場合、自分で作成するコードとそれほど変わらないため、反射はすぐに動作し始めます。







結論として、Java 7以降、 java.lang.invoke APIが登場しました。これにより、メソッドを動的に呼び出すこともできますが、まったく異なる方法で動作します。








All Articles