invokedynamic バイトコード命令は、Java 7で導入され、メソッド呼び出しのリンケージをコンパイル時ではなく実行時に遅延させることができます。() -> System.out.println("x")のようなラムダ式がコンパイルされると、javacコンパイラは、匿名内部クラスnew Runnable() { public void run() {...} }の場合に単独のMyClass$1.classファイルを生成する代わりに、LambdaMetafactory.metafactoryを指すブートストラップ引数を持つinvokedynamicを出力します。実行時に、JVMはこのブートストラップメソッドを呼び出して、ラムダボディを指すMethodHandleにリンクされたCallSiteを構築し、機能インターフェースのインスタンスを動的に作成します。このアプローチは、匿名クラスに特有の早期クラスローディング、静的初期化のオーバーヘッド、およびバイトコードの肥大化を回避し、遅延初期化を可能にし、JITコンパイラがターゲットメソッドを積極的にインライン化して最適化できるようにします。
私たちのチームは、Java 7を使用して、毎分何百万ものテレメトリエベントを処理する高スループットイベント処理パイプラインを維持していました。このシステムは、イベントフィルタのために数多くの匿名内部クラスを利用しており、数千の合成クラスの早期クラスローディングのために、著しいMetaspaceの圧力と遅い起動時間を引き起こしました。プロファイリングの結果、これらのクラスは過剰なメモリを消費し、トラフィックスパイク中に頻繁にガベージコレクションの中断を引き起こしていることがわかりました。
最初に、静的ファイナルシングルトンインスタンスを使用した明示的なStrategyパターンの実装にリファクタリングすることを検討しました。このアプローチは、インスタンスごとのアロケーションを排除し、Metaspaceの使用を完全に削減し、クラスローディングの遅延を回避します。ただし、各フィルタのためにかなりのボイラープレートコードを書く必要があり、ビジネスロジックを維持しているデータサイエンティストの可読性が大幅に低下しました。
次に、明示的なコンストラクタ呼び出しを使用して、初期化ブロックで基盤となる匿名クラスメカニズムを保持しながらJava 8構文への移行を評価しました。このアプローチは、クリーンな構文を提供しましたが、匿名クラスはコンパイル時に生成されるため、実際のパフォーマンス上の利点はありませんでした。したがって、invokedynamicの実行時の利点を得ることなく、クラスローディングのオーバーヘッドやメモリの膨張に悩まされることがわかりました。
第三に、Java 8のラムダ式とメソッド参照のみを活用し、クラス生成を実行時まで遅延させるinvokedynamicバイトコードに依存することを提案しました。この戦略は、遅延初期化による最小限のMetaspaceのフットプリントと、非キャプチャラムダに対するシングルトン最適化の可能性を約束しました。それにもかかわらず、高負荷シナリオで変数をキャプチャして予期しないアロケーションのペナルティを負わないように、慎重なコードレビューが必要でした。
最終的に、私たちは第三の解決策を選択し、キャプチャしないメソッド参照とシンプルなラムダを優先するコードガイドラインを義務付けました。この決定は、パフォーマンスの向上と保守しやすい構文のバランスを取りました。さらに、JITが頻繁に呼び出されるコールサイトを積極的に最適化できるようにしました。
デプロイ後、Metaspaceの利用率は90%低下し、アプリケーションの起動時間は40%短縮されました。ピークスループットの処理能力は著しく改善され、クラスメタデータからのGC圧力の排除により、トラフィックスパイクを優雅に処理できるようになりました。
捕捉されたラムダ式が毎回呼び出されるごとにメモリを割り当てる可能性がある理由は何ですか?非キャプチャラムダはそうでない場合、invokedynamic実装との関係はどうなりますか?
ラムダがその囲むスコープから変数をキャプチャする場合、JVMは、LambdaMetafactoryによって生成されたファクトリメソッドを介して、キャプチャされた値の異なるセットごとに生成された機能インターフェースクラスの新しいインスタンスを作成しなければならない。逆に、非キャプチャラムダの場合、ブートストラップメソッドはinvokedynamicコールサイトを、繰り返しキャッシュされたシングルトンインスタンスを返すファクトリにリンクできます。候補者はしばしば、すべてのラムダがシングルトンであると誤解し、キャプチャセマンティクスがアロケーションプロファイルを根本的に変更することを理解できず、キャプチャされた値が呼び出しごとに異なる場合、JITはこれらのアロケーションを省略できないことがあることを理解していません。
ラムダに対するinvokedynamicの使用は、クラスローディングやSecurityManagerとどのように相互作用し、特にプライベートメソッドのアクセス可能性に関してはどうですか?
invokedynamicメカニズムは、呼び出し元のコンテキストによって提供されるLookupオブジェクトを使用してリンケージ時にアクセスチェックを行います。これは、クラスローディングドメインとアクセス権限をカプセル化します。LambdaMetafactoryが実装を生成するとき、元のアクセス修飾子を尊重するMethodHandlesを使用するため、ラムダで参照されるプライベートメソッドは、その定義クラスの外部からアクセスできません。候補者はしばしば、プライベートメンバーのためにsetAccessible(true)が必要なリフレクションとこれを混同し、MethodHandlesがエンクapsulationを維持するより安全でパフォーマンスのよいパスを提供することを理解できずに実行時のSecurityManager交渉なしで実行します。
LambdaMetafactoryのaltMetafactoryメソッドの目的は何ですか?標準のmetafactoryの代わりに使用されるのはいつですか?
altMetafactoryは、基本的なmetafactoryを超える拡張機能を提供し、FLAG_SERIALIZABLEやFLAG_BRIDGESなどの追加フラグをサポートします。これにより、生成されたラムダは、Serializableのようなマーカーインターフェースを実装したり、機能インターフェースがジェネリックタイプの消去の競合を持つ場合にバイナリ互換性のためのブリッジメソッドを含めたりできます。多くの候補者は、シリアライズ可能なラムダがSerializedLambda構造をキャプチャするための追加の実行時オーバーヘッドを伴うことを理解しておらず、altMetafactoryがそれを支えることを前提にし、すべてのラムダタイプに対してシリアライズが同様に機能すると想定しています。