リフレクションは Java 言語の非常に重要な機能であり、実行中の Java プログラムがプログラムの動的な動作を観察したり、変更したりすることを可能にします。 たとえば、Class オブジェクトを通じてクラス内のすべてのメソッドを列挙できます。また、Method.setAccessible (java.lang.reflect パッケージにあり、このメソッドは AccessibleObject から継承されます) を介して Java 言語のアクセス権をバイパスし、プライベート メソッドが配置されているクラス外でメソッドを呼び出すこともできます。 リフレクションは Java で広く使用されています。開発者が日常的に使用する Java 統合開発環境 (IDE) では、この機能が使用されています。ピリオドを入力するたびに、IDE はピリオドの前の内容に基づいて、アクセス可能なフィールドまたはメソッドを動的に表示します。 もう 1 つの日常的なアプリケーションは、デバッグ中にオブジェクトのすべてのフィールドの値を列挙できる Java デバッガーです。 (図中のEclipseプロンプトは反射を使用しています) Web 開発では、さまざまな構成可能な一般的なフレームワークに触れる機会が多くあります。フレームワークのスケーラビリティを確保するために、多くの場合、Java のリフレクション メカニズムを使用して、構成ファイルに応じてさまざまなクラスをロードします。たとえば、Spring フレームワークの依存性反転 (IoC) はリフレクション メカニズムに依存しています。 しかし、多くの開発者はリフレクション メカニズムの遅さを嫌っていると思います。 Oracleのリフレクションに関するチュートリアルページ[1]でも、リフレクションの高いパフォーマンスオーバーヘッドの欠点が強調されています。 リフレクションコールの実装まず、メソッドのリフレクション呼び出し、つまり Method.invoke がどのように実装されているかを見てみましょう。
Method.invoke のソース コードを見ると、実際には MethodAccessor に処理を委任していることがわかります。 MethodAccessor は、既存の具体的な実装が 2 つあるインターフェースです。1 つはローカル メソッドを通じてリフレクション呼び出しを実装するもので、もう 1 つは委任パターンを使用するものです。覚えやすくするために、これら 2 つを「ローカル実装」と「委任実装」と呼びます。 各メソッド インスタンスの最初のリフレクション呼び出しによってデリゲート実装が生成され、デリゲート先の特定の実装はローカル実装になります。ネイティブ実装は非常に理解しやすいです。 Java 仮想マシンに入ると、Method インスタンスによって指されるメソッドの特定のアドレスが得られます。この時点では、リフレクション呼び出しは、受信パラメータを準備し、ターゲット メソッドを呼び出すことだけです。
理解を容易にするために、リフレクションがターゲット メソッドを呼び出すときにスタック トレースを出力できます。上記の v0 バージョンのコードでは、Test.target メソッドを指す Method オブジェクトを取得し、それをリフレクション呼び出しに使用します。 Test.target では、スタック トレースを出力します。 ご覧のとおり、リフレクション呼び出しは最初に Method.invoke を呼び出し、次に委任された実装 (DelegatingMethodAccessorImpl) に入り、次にローカル実装 (NativeMethodAccessorImpl) に入り、最後にターゲット メソッドに到達します。 ここで、リフレクション呼び出しではなぜ中間層として委任を使用する必要があるのか疑問に思うかもしれません。現地実装に直接引き継ぐことはできないのでしょうか? 実際、Java のリフレクション呼び出しメカニズムは、invoke 命令を直接使用してターゲット メソッドを呼び出す、動的に生成されたバイトコードの別の実装 (以下、動的実装と呼びます) も確立します。委任実装を使用する理由は、ローカル実装と動的実装を切り替えることができるためです。
動的実装はローカル実装よりも 20 倍高速に実行されます。これは、動的実装では Java から C++、そして Java に切り替える必要がないためですが、バイトコードの生成には非常に時間がかかるため、一度だけ呼び出される場合は、ローカル実装の方が 3 ~ 4 倍高速になります。 多くのリフレクション呼び出しが 1 回だけ実行されることを考慮して、Java 仮想マシンはしきい値を 15 に設定します (これは -Dsun.reflect.inflationThreshold= で調整できます)。リフレクション呼び出しの呼び出し回数が 15 未満の場合、ローカル実装が使用されます。 15 に達すると、バイトコードが動的に生成され、デリゲート実装のデリゲート オブジェクトが動的実装に切り替わります。このプロセスをインフレと呼びます。 このプロセスを観察するために、前の例を次の v1 バージョンに変更しました。リフレクション呼び出しを 20 回ループします。
ご覧のとおり、15 回目 (0 から開始) のリフレクション呼び出しで、動的実装の生成がトリガーされました。このとき、Java 仮想マシンは多数の追加クラスをロードします。その中でも最も重要なのはGeneratedMethodAccessor1(30行目)です。そして、16 番目のリフレクション呼び出しから、新しく生成された動的実装 (行 40) に切り替えます。 リフレクション呼び出しのインフレーション メカニズムは、パラメーター (-Dsun.reflect.noInflation=true) によってオフにすることができます。この方法では、委任された実装やローカル実装を使用せずに、リフレクション呼び出しの開始時に動的実装が直接生成されます。 リフレクション呼び出しのオーバーヘッド次に、リフレクション呼び出しのパフォーマンス オーバーヘッドを分析します。 先ほどの例では、Class.forName、Class.getMethod、Method.invoke 操作を順番に実行しました。このうち、Class.forName はローカル メソッドを呼び出し、Class.getMethod はクラスのパブリック メソッドを走査します。一致するものが見つからない場合は、親クラスのパブリック メソッドも走査します。ご想像のとおり、これらの操作は両方とも非常に時間がかかります。 getMethod で表されるメソッド検索操作は、検索結果のコピーを返すことに注意してください。したがって、不要なヒープ領域の消費を減らすために、ホット コードで Method 配列を返す getMethods メソッドまたは getDeclaredMethods メソッドの使用を避ける必要があります。 実際には、アプリケーションで Class.forName と Class.getMethod の結果をキャッシュすることがよくあります。したがって、以下では、リフレクション呼び出し自体のパフォーマンス オーバーヘッドにのみ焦点を当てます。 直接呼び出しとリフレクション呼び出しのパフォーマンスの差を比較するために、前の例を次の v2 バージョンに変更しました。リフレクション呼び出しを 20 億回ループします。さらに、1億回の実行ごとに時間を記録します。 ウォームアップ後のピークパフォーマンスとして、最後の 5 つの記録の平均を取得します。 (注:このパフォーマンス評価方法は厳密なものではありません。JMH を使ってパフォーマンスを測定する方法については、コラムの第 3 部で紹介します。) 私の古いラップトップでは、1 億回の直接呼び出しに約 120 ミリ秒かかります。これは呼び出されない時間と一致します。その理由は、このコードがホット ループに属しており、ジャストインタイム コンパイルもトリガーされるためです。さらに、ジャストインタイム コンパイルにより Test.target への呼び出しがインライン化されるため、呼び出しのオーバーヘッドが排除されます。
以下では、リフレクション呼び出しのパフォーマンス オーバーヘッドを比較するためのベンチマークとして 120 ミリ秒を使用します。 ターゲットメソッド Test.target は int 型のパラメータを受け取るため、リフレクション呼び出しのパラメータとして 128 を渡しましたが、計測結果はベースラインの約 2.7 倍となりました。この数値が高いか低いかに関係なく、まずはリフレクション呼び出しの前にバイトコードが何を行うかを見てみましょう。
ここでは、ループ内のリフレクション呼び出しからコンパイルされたバイトコードをキャプチャしました。ご覧のとおり、このバイトコードはリフレクション呼び出しに加えて 2 つの追加操作を実行します。
パフォーマンスのオーバーヘッドに加えて、これら 2 つの操作はヒープ メモリを占有し、GC の頻度を高める可能性もあります。 (興味があれば、仮想マシンパラメータ -XX:+PrintGC で試すことができます。) では、このオーバーヘッドをどのように排除するのでしょうか? 2 番目のオートボクシングに関しては、Java は [-128, 127] 内のすべての整数に対応する Integer オブジェクトをキャッシュします。自動的にボックス化される必要がある整数がこの範囲内にある場合、キャッシュされた Integer が返されます。それ以外の場合は、新しい Integer オブジェクトを作成する必要があります。 したがって、このキャッシュの範囲を 128 (パラメータ -Djava.lang.Integer.IntegerCache.high=128 に相当) まで拡張することができ、新しい Integer オブジェクトを作成する必要がなくなります。 あるいは、ループの外側に 128 個の自動ボックス化された Integer オブジェクトをキャッシュし、それらをリフレクション呼び出しに直接渡すこともできます。これら 2 つの方法で得られた結果は類似しており、ベンチマークの約 1.8 倍です。 ここで、可変長パラメータによって自動的に生成される最初の Object 配列に戻って見てみましょう。各リフレクション呼び出しに対応するパラメータの数は固定されているため、ループの外側で新しい Object 配列を作成し、パラメータを設定して、それをリフレクション呼び出しに直接渡すことができます。変更されたコードは、ドキュメント内の v3 バージョンを参照できます。
測定結果はさらに悪く、ベースラインの 2.9 倍でした。これはなぜでしょうか? 前の手順で自動ボクシングを解決した後、実行時に GC ステータスを確認すると、このプログラムは GC をトリガーしないことがわかります。その理由は、元のリフレクション呼び出しがインライン化されているため、ジャストインタイム コンパイラのエスケープ分析によって、新しく作成された Object 配列が非エスケープ オブジェクトとして判断されるためです。 オブジェクトがエスケープしない場合、ジャストインタイム コンパイラはスタック割り当てまたは仮想割り当てを選択できるため、ヒープ領域を占有しません。詳細については、このコラムの後半で説明します。 ループの外で新しい配列が作成されると、ジャストインタイムコンパイラは配列が途中で変更されるかどうか確信が持てないため、配列にアクセスする操作を最適化できず、その損失に見合う価値がありません。 これまでの最高記録は1.8倍です。それで、さらに改善できるのでしょうか? 先ほど、リフレクション呼び出しのインフレーション メカニズムをオフにして、委任された実装をキャンセルし、動的実装を直接使用できることを説明しました。さらに、各リフレクション呼び出しではターゲット メソッドの権限がチェックされますが、このチェックは Java コードでオフにすることもできます。これら 2 つのメカニズムをオフにすると、ベースラインの約 1.3 倍の測定値を持つ v4 バージョンが得られます。
この時点で、リフレクションコールから得られる水分は基本的にすべて絞り出されています。次に、リフレクション呼び出しのパフォーマンス オーバーヘッドについて説明します。 まず、この例では、リフレクション呼び出しが非常に高速である理由は、主にジャストインタイム コンパイラでのメソッドのインライン化によるものです。インフレがオフの場合、インライン化のボトルネックとなるのは、Method.invoke メソッド内の MethodAccessor.invoke メソッドの呼び出しです。 メソッドのインライン化の具体的な実装については、後の記事で紹介します。結論は次のとおりです。実稼働環境では、複数の GeneratedMethodAccessor、つまり動的実装に対応する複数の異なるリフレクション呼び出しが頻繁に発生します。 上記の呼び出しポイントに対する Java 仮想マシンの型プロファイル (注:invokevirtual またはinvokeinterface の場合、Java 仮想マシンは呼び出し元の特定の型を記録します。これを型プロファイルと呼びます) は、同時に多くのクラスを記録できないため、テスト対象の反映された呼び出しがインライン化されない可能性があります。
上記の v5 バージョンでは、テスト ループの前に polluteProfile メソッドを呼び出しました。このメソッドは、他の 2 つのメソッドをリフレクション呼び出しし、2000 回ループします。 テストサイクルは変更されません。測定結果はベンチマークの約6.7倍です。つまり、Method.invoke メソッドの型プロファイルが誤って乱れると、パフォーマンスのオーバーヘッドが 1.3 倍から 6.7 倍に増加します。 インライン化の欠如に加えて、エスケープ分析が効果的でなくなったことも、速度低下のもう 1 つの理由です。現時点では、バージョン v3 のソリューションを採用し、ループの外側でパラメータ配列を構築し、それをリフレクション呼び出しに直接渡すことができます。このようにして測定した結果はベンチマークの約5.2倍となります。 さらに、Java 仮想マシンが各呼び出しに対して記録できるタイプの数を増やすこともできます (仮想マシン パラメータ -XX:TypeProfileWidth に対応し、デフォルト値は 2 ですが、ここでは 3 に設定されています)。最終的な測定結果はベンチマークの約2.8倍です。当初の1.3倍からはまだ一定の距離はありますが、6.7倍よりはずっと良いです。 まとめと実践デフォルトでは、メソッドへのリフレクション呼び出しは、メソッド呼び出しを実行するためにローカル実装に委任されます。 15 回を超える呼び出しの後、デリゲート実装はデリゲート オブジェクトを動的実装に切り替えます。この動的実装のバイトコードは自動的に生成され、invoke 命令を直接使用してターゲット メソッドを呼び出します。 メソッドのリフレクション呼び出しは、パフォーマンスのオーバーヘッドを大幅に増加させます。主な理由は 3 つあります。可変長パラメータ メソッドによって発生するオブジェクト配列、基本型の自動ボックス化とアンボックス化、そして最も重要なメソッドのインライン化です。 この記事の実践的な部分では、最後のコード スニペットの polluteProfile メソッドの 2 つの Method オブジェクトを、「target」という名前を取得するメソッドに変更できます。取得されたこれら 2 つの Method オブジェクトは同じですか (==)?それらは等しいですか (.equals(…))?ランニングの結果にどのような影響があるのでしょうか?
|
<<: サンライフファイナンシャル、デジタルサービスの変革に向けた長期戦略クラウドプロバイダーとしてアマゾンウェブサービスを選択
>>: この記事では、JVMメモリモデルについて詳しく説明します。
[[403814]]この記事はWeChatの公開アカウント「Java Geek Technology...
2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています近年、経済...
ジェレミー・リンは全米バスケットボール協会で急速に人気者となり、「リン・トルネード」もインターネット...
はじめに:SEO最適化は現在最も費用対効果の高いインターネットマーケティング手法であり、最もホットな...
01 人類文明の発展の要素原始社会史家モーガンは、その代表作『古代社会』の中でこう述べている。「野蛮...
Sugarhosts は私たちにとって馴染みのある IDC です。皆さんも最初に仮想ホストに触れたの...
onetechcloud では現在、ビジネス開始のための特別プロモーションを実施しており、月払いの場...
1. SNS軍の地下産業チェーンが、タオバオの偽注文は単なる空想であることを暴露最近、SNS軍はさま...
ショートビデオ、セルフメディア、インフルエンサーのためのワンストップサービス毎日、Weiboには私た...
2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますマーケティ...
春節までまだ1週間ありますが、春節のウェブサイトのメンテナンスとマーケティングはすでに一部のウェブサ...
geecdn(2017年創業)が、いち早く「ブラックフライデー」プロモーションを実施。フランスのOV...
Dedicated Mindsは、イギリスに登録された有限会社です(登録番号08536083)。ウェ...
大王データは「年末期間限定特別フラッシュセールイベント」を開催しています。中国の武漢・十堰・温州と香...
2018年5月、上場企業Hanye Co., Ltd.はQuantum Cloudを買収するために3...