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

すべての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オブジェクトを保存することをお勧めします。


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



-Dsun.reflect.noInflation=trueを有効にし、JNIのみを使用する場合(このために大きなしきい値-Dsun.reflect.inflationThreshold=100000000を設定する)、テストで何が起こるかを見てみましょう。


(アクセス可能)(非公開)デフォルトインフレ率JNIのみ
本当本当5,062±0,0564.935±0.375195,960±1,873
本当5.042±0.0324.914±0.329194,722±1,151
本当6.078±0.0395.638±0.050196.196±0.910
5.835±0.0285.520±0.042194.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,0566.848±0.0314.935±0.3756.509±0.032195,960±1,873
本当5.042±0.0326.847±0.0354.914±0.3296.490±0.037194,722±1,151
本当6.078±0.0397.855±0.0405.638±0.0507.661±0.049196.196±0.910
5.835±0.0287.568±0.0465.520±0.0427.111±0.058194.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.0316.509±0.032195,960±1,873
リセット+呼び出し227.133±9.159100,195.746±2060.810236.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スローします。 別のクラスのオブジェクトを渡した場合、 IllegalArgumentExceptionperson.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代わりに、 ByteShortCharacterまたはIntegerを渡す権利があり、これらはすべて変換する必要があることに注意してください。 これは、変換が行われている場所です。 このようなブロックは、拡張変換が可能な各プリミティブ引数ごとに追加されます。 また、キャッチがNullPointerExceptionによってキャッチされる理由も明らかです。これは、アンボックス中に発生する可能性があり、その後IllegalArgumentExceptionIllegalArgumentException必要があります。 しかし、メソッドは静的であるという事実のため、 targetパラメーターの内容は気にしません。 さて、このメソッドはvoid返すため、 return null行がありました。 このすべての魔法は、 MethodAccessorGenerator.emitInvokeにきちんと描かれています


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


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



Source: https://habr.com/ru/post/J318418/


All Articles