Java SE 8のラムダ式はどう実現されたのか?──実装の経緯、内部的な仕組みを理解する
Java SE 8で導入されたラムダ式は、どのような仕組みで実現されているのだろうか? ラムダ式をより深く理解したいと考える読者にとって、これは気になるテーマの1つだろう。その秘密をお教えしよう。
日本オラクル Java SE サステイニング エンジニアリングのデイビッド・バック氏
Java SE 8で導入されたラムダ式は、Javaの言語仕様の歴史において最大級の変更だとされている。ただし、これはあくまでもJava言語の文法レベルの話であり、Javaプログラムを実行するJava仮想マシン(JVM)に新たな仕組みが導入されたわけではない。つまり、言語仕様に関する旧バージョンとの相違はコンパイラとランタイムが吸収し、バイトコードレベルでの互換性は保たれているということだ。
それでは、このような大きな変更を、既存のJVMの仕組みの中でどのように実現したのだろうか。それを知ることは、ラムダ式をより深く理解する手助けとなるかもしれない。Java SE 8のラムダ式実現の経緯と仕組みについて、日本オラクルのデイビッド・バック氏(Java SE サステイニング エンジニアリング)による解説をお届けする。
※ 本記事は、日本オラクルが2015年4月に開催した「Java Day Tokyo 2015」におけるデイビッド・バック氏のセッション「Lambda: A Peek Under The Head」の内容を基に構成しています。
ラムダ式の実装を理解するための前提知識
初めに、本記事の狙いを明確にしておきたい。ここでは、読者が次の2点についての知識を得ることを目的とする。
- ラムダ式に該当するバイトコードを理解すること
- ラムダ式のパフォーマンスの影響を把握すること
これらの目的を達成するためには、その前提として次の知識が必要になる。
- ラムダ式(Java言語レベル)
- Javaバイトコード
- JSR 292(動的なメソッド呼び出し)
このうち、JSR 292については、一般の開発者はなじみが薄いかもしれない。そこで、本題に入る前に、まずJSR 292、つまりラムダ式の内部的な実現方式について説明する。
JSR 292で追加された新機能
「JSR 292: Supporting Dynamically Typed Languages on the Java Platform」は、もともとはOpenJDKの「Da Vinci Machine Project」と呼ばれるプロジェクトとして発足したものだ。同プロジェクトの目的は、JVM上でJava以外の言語のランタイムをより簡単に実装できるようにすることにある。その背景には、JRubyやGroovy、Jythonなど、JVM上で動作する軽量言語が多数登場してきたことがあった。
バック氏は、「JVM上にJava以外の言語のランタイムを実装する際、特にネックとなったのが、動的な型付けのメソッド呼び出しをサポートしていないことでした」と振り返る。従来のJVMには、次の4種類のメソッド呼び出しの命令が用意されていた。
- invokevirtual: インスタンス・メソッド
- invokeinterface: インタフェースのメソッド
- invikestatic: 静的メソッド
- invokespecial: その他のメソッド(コンストラクタ、スーパークラス、privateメソッドなど)
「これら4種類の命令は、Java言語に限って言えば、極めて適切なものです。しかし、他の言語にとっては必ずしも十分ではない場合がありました。JSR 292では、その"十分ではない場合"を改善することを目指したのです」(バック氏)
これらの命令は、ターゲットのメソッドを特定して呼び出すことが前提となっている。Java言語は静的型付け言語であり、コンパイルの段階でターゲットを明示して呼び出しを行うバイトコードを生成する。したがって、上記4種類の命令ですべての要件を満たすことができる。
一方、動的型付け言語では、ターゲットの特定をプログラム実行時まで遅らせる必要が生じる。しかし、既存のJVMにはそのような命令が存在しないため、各ランタイム側でメソッド・ディスパッチをエミュレートして動的メソッド呼び出しを仮想的に実現する必要があった。だが、この方法はエミュレーション処理のオーバーヘッドが高いことに加えて、JITコンパイラによる最適化が働かず、プログラムのパフォーマンスが悪いという問題を抱えていた。
バック氏によれば、JSR 292では当初、Java言語以外のメソッド呼び出しのロジックも直接サポートするようにJVMを拡張する予定であった。しかし、ディスパッチのロジックが言語ごとに異なるため、単にサポートするロジックを増やすだけのアプローチでは対応できなかったという。そこで採用された解決策が、「JVM側ではディスパッチのロジックを固定せず、代わりにディスパッチのロジックを定義するAPIを提供する」というものであった。
この方針に基づいてJSR 292で追加されたのが、次の2つの機能である。
- java.lang.invoke API: ディスパッチ・ロジックを定義するためのAPI(Java API側の修正)
- invokedynamicバイトコード命令: invoke APIで定義したロジックを使ってディスパッチを行う命令(JVM側の修正)
invokedynamicバイトコード命令は、通称「indy(インディ)」と呼ばれる。このindy命令の追加は、「JVMの歴史上、非常に衝撃的な出来事」(バック氏)であった。
「Java言語には、この命令に該当する機能がありません。そのため当初、この命令がJava言語で使われる予定はありませんでした。そのことを踏まえると、この仕様変更は2つの点で歴史的な出来事だったと言えます。1つは、JVMが誕生して以来、初めて新しい命令が追加されたこと。もう1つは、初めてJava以外の言語のためにJVMが変更されたことです」(バック氏)
invokedynamicを使ったメソッド呼び出しの仕組み
java.lang.invoke APIとindy命令を使ったメソッド・ディスパッチの仕組みを理解するうえでは、invoke APIの次の3つの要素が重要となる。
- クラスMethodHandle
- クラスCallSite
- bootstrapメソッド
このうち、MethodHandleはメソッドを指定するためのクラスであり、型記述子とは無関係にメソッドを指定できる仕組みを提供する。「これは関数ポインタのような働きをするクラスだと考えればご理解いただきやすいでしょう」とバック氏は話す。
また、クラスCallSiteはバイトコード中のindy命令の呼び出しをJava APIとして具現化したものであり、indy命令が実行されるとCallSiteのインスタンスが作られる。CallSiteはMethodHandleを保持しており、ディスパッチされたメソッドを特定することができる。
bootstrapメソッドはindy命令の1回目の実行時に呼び出されるメソッドであり、呼び出し元のindy命令にひも付いたCallSiteオブジェクトを生成して返す。CallSiteオブジェクトを生成する際にはMethodHandleの初期値を設定する。
以上を踏まえ、1つのindy命令のライフサイクルをまとめると、次のようになる。
【1回目の実行】
(1)特定のindy命令が初めて実行される
(2)bootstrapメソッドが呼び出され、(1)のindy命令に該当するメソッドを選ぶ
(3)bootstrapメソッドが(1)のindy命令に該当するCallSiteオブジェクトを生成して返す
(4)CallSiteのMethodHandleが指定するメソッドへジャンプする
【2回目以降の実行】
(1)CallSiteのMethodHandleが指定するメソッドへジャンプする
このライフサイクルからもわかるように、bootstrapメソッドが呼ばれるのは初回のみであり、2回目以降の呼び出しは初回よりも速く処理される。
また、この仕組みのもう1つの特筆すべき点として、「CallSiteが持つMethodHandleは、言語のランタイム側から自由に変更できる」ということが挙げられる。例えば、引数のオブジェクトの型が異なる場合や、メソッドが動的に変更された場合などには、CallSiteを修正してMethodHandleが指示するメソッドを変えればよい。「メソッドへのリンク処理をランタイム側に任せてしまおう」という発想である。
クイックポール
WebLogic Channelで最も読んでみたい記事のテーマを以下よりお選びください
トピックス
-
デジタル・トランスフォーメーション時代のクラウド選定基準とは
(Digital Transformationへリンク)
-
DBセキュリティ見直しにも影響するマイナンバー安全管理の“要件”と“盲点”
(@IT Specialへリンク)
-
2時間で作るカスタマージャーニーマップ――実例とともに考える新しい「おもてなし」のカタチ
(WEB担当者Forumへリンク)