ScalaJVMバイトコードで手を汚す

公開: 2022-03-11

Scala言語は、機能とオブジェクト指向のソフトウェア開発原則の優れた組み合わせと、実績のあるJava仮想マシン(JVM)上での実装のおかげで、過去数年にわたって人気を博し続けています。

ScalaはJavaバイトコードにコンパイルされますが、Java言語の認識されている欠点の多くを改善するように設計されています。 完全な関数型プログラミングのサポートを提供するScalaのコア構文には、Javaプログラマーが明示的に構築する必要のある多くの暗黙的な構造が含まれており、その中にはかなり複雑なものもあります。

Javaバイトコードにコンパイルされる言語を作成するには、Java仮想マシンの内部動作を深く理解する必要があります。 Scalaの開発者が成し遂げたことを理解するには、内部を調べて、効率的で効果的なJVMバイトコードを生成するためにScalaのソースコードがコンパイラーによってどのように解釈されるかを調べる必要があります。

これらすべてがどのように実装されているかを見てみましょう。

前提条件

この記事を読むには、Java仮想マシンのバイトコードの基本的な理解が必要です。 完全な仮想マシンの仕様は、Oracleの公式ドキュメントから入手できます。 この記事を理解するために仕様全体を読むことは重要ではないため、基本を簡単に紹介するために、記事の下部に短いガイドを用意しました。

JVMの基本に関するクラッシュコースを読むには、ここをクリックしてください。

以下に示す例を再現するためにJavaバイトコードを分解し、さらに調査を進めるには、ユーティリティが必要です。 Java Development Kitには、独自のコマンドラインユーティリティjavapが用意されており、ここで使用します。 javapがどのように機能するかの簡単なデモンストレーションは、下部のガイドに含まれています。

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラーの作業インストールが必要です。 この記事はScala2.11.7を使用して書かれました。 Scalaのバージョンが異なると、バイトコードがわずかに異なる場合があります。

デフォルトのゲッターとセッター

Javaの規則では、パブリック属性のゲッターメソッドとセッターメソッドが常に提供されますが、Javaプログラマーは、それぞれのパターンが数十年にわたって変更されていないにもかかわらず、これらを自分で作成する必要があります。 対照的に、Scalaはデフォルトのゲッターとセッターを提供します。

次の例を見てみましょう。

 class Person(val name:String) { }

クラスPersonの内部を見てみましょう。 このファイルをscalacでコンパイルすると、 $ javap -p Person.classを実行すると次のようになります。

 Compiled from "Person.scala" public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Scalaクラスのフィールドごとに、フィールドとそのgetterメソッドが生成されていることがわかります。 フィールドはプライベートでファイナルですが、メソッドはパブリックです。

Personソースでvalvarに置き換えて再コンパイルすると、フィールドのfinalの修飾子が削除され、setterメソッドも追加されます。

 Compiled from "Person.scala" public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

クラス本体内でvalまたはvarが定義されている場合、対応するプライベートフィールドとアクセサーメソッドが作成され、インスタンスの作成時に適切に初期化されます。

クラスレベルのvalフィールドとvarフィールドのこのような実装は、中間値を格納するためにクラスレベルでいくつかの変数が使用され、プログラマーが直接アクセスしない場合、そのような各フィールドの初期化により、1つから2つのメソッドが追加されることに注意してください。クラスのフットプリント。 このようなフィールドにprivate修飾子を追加しても、対応するアクセサーが削除されるわけではありません。 彼らはただプライベートになります。

変数と関数の定義

メソッドm()があり、この関数への3つの異なるScalaスタイルの参照を作成するとします。

 class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

mへのこれらの参照のそれぞれはどのように構築されますか? いずれの場合も、 mはいつ実行されますか? 結果のバイトコードを見てみましょう。 次の出力は、 javap -v Person.classの結果を示しています(多くの余分な出力を省略しています)。

 Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object."<init>":()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

定数プールでは、メソッドm()への参照がインデックス#30に格納されていることがわかります。 コンストラクターコードでは、このメソッドが初期化中に2回呼び出され、命令invokevirtual #30が最初にバイトオフセット11に表示され、次にオフセット19に表示されることがわかります。最初の呼び出しの後に命令putfield #22が続き、このメソッドは、定数プールのインデックス#22によって参照されるフィールドm1に適用されます。 2番目の呼び出しの後に同じパターンが続きます。今回は、定数プールの#24でインデックス付けされたフィールドm2に値を割り当てます。

つまり、 valまたはvarで定義された変数にメソッドを割り当てると、その変数にメソッドの結果のみが割り当てられます。 作成されたメソッドm1()m2()は、これらの変数の単なるゲッターであることがわかります。 var m2の場合、セッターm2_$eq(int)が作成されていることもわかります。これは、他のセッターと同じように動作し、フィールドの値を上書きします。

ただし、キーワードdefを使用すると、異なる結果が得られます。 返すフィールド値をフェッチするのではなく、メソッドm3()にはinvokevirtual #30命令も含まれています。 つまり、このメソッドが呼び出されるたびに、 m()が呼び出され、このメソッドの結果が返されます。

したがって、ご覧のとおり、Scalaはクラスフィールドを操作する3つの方法を提供し、これらはキーワードvalvar 、およびdefを介して簡単に指定できます。 Javaでは、必要なセッターとゲッターを明示的に実装する必要があり、そのような手動で記述されたボイラープレートコードは、表現力がはるかに低く、エラーが発生しやすくなります。

怠惰な値

遅延値を宣言すると、より複雑なコードが生成されます。 以前に定義したクラスに次のフィールドを追加したと仮定します。

 lazy val m4 = m

javap -p -v Person.classを実行すると、次のようになります。

 Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

この場合、フィールドm4の値は、必要になるまで計算されません。 レイジー値を計算するために特別なプライベートメソッドm4$lzycompute()が生成され、その状態を追跡するためにフィールドbitmap$0が生成されます。 メソッドm4()は、このフィールドの値が0であるかどうかをチェックし、 m4がまだ初期化されていないことを示します。この場合、 m4$lzycompute()が呼び出され、 m4にデータが入力され、その値が返されます。 このプライベートメソッドもbitmap$0の値を1に設定するため、次にm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scala遅延値への最初の呼び出しの結果。

Scalaがここで生成するバイトコードは、スレッドセーフで効果的であるように設計されています。 スレッドセーフにするために、レイジーコンピューティングメソッドはmonitorenter / monitorexit命令のペアを使用します。 この同期のパフォーマンスオーバーヘッドは、遅延値の最初の読み取り時にのみ発生するため、この方法は引き続き有効です。

遅延値の状態を示すために必要なビットは1つだけです。 したがって、レイジー値が32個以下の場合、単一のintフィールドでそれらすべてを追跡できます。 ソースコードで複数の遅延値が定義されている場合、上記のバイトコードは、この目的のためにビットマスクを実装するためにコンパイラによって変更されます。

繰り返しになりますが、Scalaを使用すると、Javaで明示的に実装する必要がある特定の種類の動作を簡単に利用できるため、労力を節約し、タイプミスのリスクを減らすことができます。

価値として機能する

それでは、次のScalaソースコードを見てみましょう。

 class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output("Hello"); } }

Printerクラスには、 String => Unit型の1つのフィールドoutputがあります。これは、 Stringを受け取り、 Unit型のオブジェクトを返す関数です(Javaのvoidと同様)。 mainメソッドでは、これらのオブジェクトの1つを作成し、このフィールドを、指定された文字列を出力する無名関数として割り当てます。

このコードをコンパイルすると、4つのクラスファイルが生成されます。

ソースコードは4つのクラスファイルにコンパイルされます。

Hello.classは、mainメソッドがHello$.main()を呼び出すだけのラッパークラスです。

 public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

非表示のHello$.classには、mainメソッドの実際の実装が含まれています。 そのバイトコードを確認するには、コマンドシェルの規則に従って、 $を正しくエスケープし、特殊文字として解釈されないようにしてください。

 public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1."<init>":()V 11: invokespecial #22 // Method Printer."<init>":(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string "Hello" 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

このメソッドは、 Printerを作成します。 次に、 Hello$$anonfun$1を作成します。これには、無名関数s => println(s)が含まれています。 Printerは、このオブジェクトをoutputフィールドとして初期化されます。 次に、このフィールドはスタックにロードされ、オペランド"Hello"で実行されます。

以下の無名関数クラスHello$$anonfun$1.classを見てみましょう。 apply()メソッドを実装することで、ScalaのFunction1を( AbstractFunction1として)拡張していることがわかります。 実際には、2つのapply()メソッドを作成します。1つはもう1つをラップし、タイプチェック(この場合は入力がString )を実行し、無名関数を実行します( println()を使用して入力を出力します)。

 public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1<java.lang.String, scala.runtime.BoxedUnit> implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

上記のHello$.main()メソッドを振り返ると、オフセット21で、無名関数の実行がそのapply( Object )メソッドの呼び出しによってトリガーされていることがわかります。

最後に、完全を期すために、 Printer.classのバイトコードを見てみましょう。

 public class Printer // ... // field private final scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output; // field getter public scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1<java.lang.String, scala.runtime.BoxedUnit>); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object."<init>":()V 9: return

ここの無名関数は他のval変数と同じように扱われていることがわかります。 これはクラスフィールドoutputに格納され、getter output()が作成されます。 唯一の違いは、この変数がScalaインターフェースscala.Function1AbstractFunction1が行う)を実装する必要があることです。

したがって、このエレガントなScala機能のコストは、値として使用できる単一の無名関数を表現および実行するために作成された、基礎となるユーティリティクラスです。 特定のアプリケーションにとってそれが何を意味するかを理解するには、そのような機能の数とVM実装の詳細を考慮する必要があります。

Scalaの内部を理解する:この強力な言語がJVMバイトコードにどのように実装されているかを探ります。
つぶやき

Scalaの特性

Scalaの特性は、Javaのインターフェースに似ています。 次の特性は、2つのメソッドシグネチャを定義し、2番目のメソッドシグネチャのデフォルトの実装を提供します。 それがどのように実装されているか見てみましょう:

 trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) } 

ソースコードは2つのクラスファイルにコンパイルされます。

2つのエンティティが生成されます。両方のメソッドを宣言するインターフェイスであるSimilarity.classと、デフォルトの実装を提供する合成クラス、 Similarity$class.classです。

 public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
 public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

クラスがこの特性を実装し、メソッドisNotSimilarを呼び出すと、Scalaコンパイラはバイトコード命令invokestaticを生成して、付随するクラスによって提供される静的メソッドを呼び出します。

複雑なポリモーフィズムと継承構造は、特性から作成できます。 たとえば、複数のトレイトと実装クラスはすべて、同じシグネチャを持つメソッドをオーバーライドし、 super.methodName()を呼び出して次のトレイトに制御を渡すことができます。 Scalaコンパイラがそのような呼び出しに遭遇すると、次のようになります。

  • この呼び出しによって想定される正確な特性を決定します。
  • トレイトに定義された静的メソッドのバイトコードを提供する付随するクラスの名前を決定します。
  • 必要なinvokestatic命令を生成します。

したがって、特性の強力な概念がJVMレベルで実装されており、大きなオーバーヘッドが発生しないことがわかります。Scalaプログラマーは、実行時にコストがかかりすぎることを心配せずにこの機能を楽しむことができます。

シングルトン

Scalaは、キーワードobjectを使用してシングルトンクラスの明示的な定義を提供します。 次のシングルトンクラスについて考えてみましょう。

 object Config { val home_dir = "/home/user" }

コンパイラは2つのクラスファイルを生成します。

ソースコードは2つのクラスファイルにコンパイルされます。

Config.classは非常に単純なものです。

 public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

これは、シングルトンの機能を組み込む合成Config$クラスのデコレータにすぎません。 そのクラスをjavap -p -cで調べると、次のバイトコードが生成されます。

 public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method "<init>":()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object."<init>":()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value "/home/user" and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

以下で構成されています。

  • 他のオブジェクトがこのシングルトンオブジェクトにアクセスするための合成変数MODULE$
  • 静的初期化子{}<clinit> 、クラス初期化子とも呼ばれます)とプライベートメソッドConfig$は、 MODULE$を初期化し、そのフィールドをデフォルト値に設定するために使用されます。
  • 静的フィールドhome_dirのgetterメソッド。 この場合、これは1つの方法にすぎません。 シングルトンにさらに多くのフィールドがある場合は、可変フィールドのセッターだけでなく、ゲッターも多くなります。

シングルトンは人気があり便利なデザインパターンです。 Java言語は、言語レベルでそれを指定する直接的な方法を提供しません。 むしろ、Javaソースに実装するのは開発者の責任です。 一方、Scalaは、 objectキーワードを使用してシングルトンを明示的に宣言するための明確で便利な方法を提供します。 ボンネットの下を見るとわかるように、手頃な価格で自然な方法で実装されています。

結論

これで、Scalaがいくつかの暗黙的および関数型プログラミング機能を洗練されたJavaバイトコード構造にコンパイルする方法を見てきました。 Scalaの内部動作を垣間見ることで、Scalaの力をより深く理解することができ、この強力な言語を最大限に活用するのに役立ちます。

また、自分たちで言語を探索するためのツールもあります。 ケースクラス、カリー化、リスト内包表記など、この記事では取り上げられていないScala構文の多くの便利な機能があります。 これらの構造のScalaの実装を自分で調査することをお勧めします。そうすれば、次のレベルのScala忍者になる方法を学ぶことができます。


Java仮想マシン:クラッシュコース

Javaコンパイラと同様に、Scalaコンパイラはソースコードを.classファイルに変換します。このファイルには、Java仮想マシンによって実行されるJavaバイトコードが含まれています。 2つの言語が内部でどのように異なるかを理解するには、両方が対象としているシステムを理解する必要があります。 ここでは、Java仮想マシンアーキテクチャのいくつかの主要な要素、クラスファイル構造、およびアセンブラの基本について簡単に説明します。

このガイドでは、上記の記事に沿ってフォローできるようにするための最小限の内容のみを取り上げていることに注意してください。 JVMの多くの主要コンポーネントについてはここでは説明していませんが、完全な詳細については、こちらの公式ドキュメントを参照してください。

javapを使用したクラスファイルの逆コンパイル
コンスタントプール
フィールドテーブルとメソッドテーブル
JVMバイトコード
メソッド呼び出しと呼び出しスタック
オペランドスタックでの実行
ローカル変数
トップに戻る

javapを使用したクラスファイルの逆コンパイル

Javaには、 .classファイルを人間が読める形式に逆コンパイルするjavapコマンドラインユーティリティが付属しています。 ScalaとJavaのクラスファイルはどちらも同じJVMを対象としているため、 javapを使用してScalaによってコンパイルされたクラスファイルを調べることができます。

次のソースコードをコンパイルしてみましょう。

 // RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( "Calculating perimeter..." ) return sideLength * this.numSides } }

これをscalac RegularPolygon.scala RegularPolygon.class生成されます。 次にjavap RegularPolygon.classを実行すると、次のように表示されます。

 $ javap RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

これはクラスファイルの非常に単純な内訳であり、クラスのパブリックメンバーの名前とタイプを単純に示しています。 -pオプションを追加すると、プライベートメンバーが含まれます。

 $ javap -p RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

これはまだ多くの情報ではありません。 メソッドがJavaバイトコードでどのように実装されているかを確認するために、 -cオプションを追加しましょう。

 $ javap -p -c RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object."<init>":()V 9: return }

それはもう少し面白いです。 ただし、実際に全体を把握するには、 javap -p -v RegularPolygon.classのように、 -vまたは-verboseオプションを使用する必要があります。

Javaクラスファイルの完全な内容。

ここで、最終的にクラスファイルに実際に何が含まれているかがわかります。 これはどういう意味ですか? 最も重要な部分のいくつかを見てみましょう。

コンスタントプール

C ++アプリケーションの開発サイクルには、コンパイルとリンケージの段階が含まれます。 リンケージは実行時に発生するため、Javaの開発サイクルは明示的なリンケージステージをスキップします。 クラスファイルは、このランタイムリンクをサポートする必要があります。 つまり、ソースコードが任意のフィールドまたはメソッドを参照する場合、結果のバイトコードは関連する参照をシンボリック形式で保持し、アプリケーションがメモリにロードされ、ランタイムリンカーによって実際のアドレスを解決できるようになったら逆参照できるようにする必要があります。 このシンボリックフォームには、次のものが含まれている必要があります。

  • クラス名
  • フィールド名またはメソッド名
  • タイプ情報

クラスファイル形式の仕様には、定数プールと呼ばれるファイルのセクションが含まれています。これは、リンカが必要とするすべての参照のテーブルです。 さまざまなタイプのエントリが含まれています。

 // ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

各エントリの最初のバイトは、エントリのタイプを示す数値タグです。 残りのバイトは、エントリの値に関する情報を提供します。 バイト数とその解釈の規則は、最初のバイトで示されるタイプによって異なります。

たとえば、定数整数365を使用するJavaクラスには、次のバイトコードを持つ定数プールエントリが含まれる場合があります。

 x03 00 00 01 6D

最初のバイトx03は、エントリタイプCONSTANT_Integerを識別します。 これは、次の4バイトに整数の値が含まれていることをリンカに通知します。 (16進数の365はx16Dであることに注意してください)。 これが定数プールの14番目のエントリである場合、 javap -vは次のようにレンダリングします。

 #14 = Integer 365

多くの定数型は、定数プール内の他の場所にある、より「プリミティブな」定数型への参照で構成されています。 たとえば、サンプルコードには次のステートメントが含まれています。

 println( "Calculating perimeter..." )

文字列定数を使用すると、定数プールに2つのエントリが生成されます。1つはCONSTANT_String型のエントリで、もう1つはCONSTANT_Utf8型のエントリです。 タイプConstant_UTF8のエントリには、文字列値の実際のUTF8表現が含まれています。 タイプCONSTANT_Stringのエントリには、 CONSTANT_Utf8エントリへの参照が含まれています。

 #24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

タイプUtf8のエントリを参照し、タイプStringのエントリではない他のタイプの定数プールエントリがあるため、このような複雑さが必要です。 たとえば、クラス属性への参照はCONSTANT_Fieldrefタイプを生成します。これには、クラス名、属性名、および属性タイプへの一連の参照が含まれます。

 #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

定数プールの詳細については、JVMのドキュメントを参照してください。

フィールドテーブルとメソッドテーブル

クラスファイルには、クラスで定義された各フィールド(つまり、属性)に関する情報を含むフィールドテーブルが含まれています。 これらは、フィールドの名前とタイプ、およびアクセス制御フラグとその他の関連データを説明する定数プールエントリへの参照です。

同様のメソッドテーブルがクラスファイルに存在します。 ただし、名前とタイプの情報に加えて、非抽象メソッドごとに、JVMによって実行される実際のバイトコード命令と、以下で説明するメソッドのスタックフレームによって使用されるデータ構造が含まれます。

JVMバイトコード

JVMは、独自の内部命令セットを使用して、コンパイルされたコードを実行します。 -cオプションを指定してjavapを実行すると、コンパイルされたメソッドの実装が出力に含まれます。 この方法でRegularPolygon.classファイルを調べると、 getPerimeter()メソッドの次の出力が表示されます。

 public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

実際のバイトコードは次のようになります。

 xB2 00 17 x12 19 xB6 00 1D x27 ...

各命令は、JVM命令を識別する1バイトのオペコードで始まり、特定の命令の形式に応じて、操作対象の0個以上の命令オペランドが続きます。 これらは通常、定数値、または定数プールへの参照のいずれかです。 javapは、バイトコードを人間が読める形式に変換して次のように表示します。

  • オフセット、またはコード内の命令の最初のバイトの位置。
  • 命令の人間が読める名前、またはニーモニック
  • オペランドの値(ある場合)。

#23などのポンド記号で表示されるオペランドは、定数プール内のエントリーへの参照です。 ご覧のとおり、 javapは出力に役立つコメントも生成し、プールから正確に参照されているものを識別します。

以下に、いくつかの一般的な手順について説明します。 完全なJVM命令セットの詳細については、ドキュメントを参照してください。

メソッド呼び出しと呼び出しスタック

各メソッド呼び出しは、ローカルで宣言された変数やメソッドに渡された引数など、独自のコンテキストで実行できる必要があります。 一緒に、これらはスタックフレームを構成します。 メソッドを呼び出すと、新しいフレームが作成され、呼び出しスタックの一番上に配置されます。 メソッドが戻ると、現在のフレームが呼び出しスタックから削除されて破棄され、メソッドが呼び出される前に有効だったフレームが復元されます。

スタックフレームには、いくつかの異なる構造が含まれています。 2つの重要なものは、次に説明するオペランドスタックローカル変数テーブルです。

JVMコールスタック。

オペランドスタックでの実行

多くのJVM命令は、フレームのオペランドスタックで動作します。 これらの命令は、バイトコードで定数オペランドを明示的に指定するのではなく、オペランドスタックの最上位にある値を入力として受け取ります。 通常、これらの値はプロセスでスタックから削除されます。 一部の命令では、スタックの一番上に新しい値を配置します。 このようにして、JVM命令を組み合わせて複雑な操作を実行できます。 たとえば、次の式を使用します。

 sideLength * this.numSides

getPerimeter()メソッドで次のようにコンパイルされます。

 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 

JVM命令は、オペランドスタックを操作して、複雑な機能を実行できます。

  • 最初の命令dload_1は、オブジェクト参照をローカル変数テーブル(次に説明)のスロット1からオペランドスタックにプッシュします。 この場合、これはメソッド引数sideLength 。-次の命令aload_0は、ローカル変数テーブルのスロット0にあるオブジェクト参照をオペランドスタックにプッシュします。 実際には、これはほとんどの場合、現在のクラスであるthisへの参照です。
  • これにより、インスタンスメソッドnumSides()を実行する次の呼び出しinvokevirtual #31のスタックが設定されます。 invokevirtualは、最上位のオペランド( thisへの参照)をスタックからポップして、どのクラスからメソッドを呼び出さなければならないかを識別します。 メソッドが戻ると、その結果はスタックにプッシュされます。
  • この場合、返される値( numSides )は整数形式です。 別のdouble値を乗算するには、double浮動小数点形式に変換する必要があります。 命令i2dは、整数値をスタックからポップし、浮動小数点形式に変換して、スタックにプッシュバックします。
  • この時点で、スタックにはthis.numSidesの浮動小数点結果が先頭に含まれ、その後にメソッドに渡されたsideLength引数の値が続きます。 dmulは、これらの上位2つの値をスタックからポップし、それらに対して浮動小数点乗算を実行して、結果をスタックにプッシュします。

メソッドが呼び出されると、スタックフレームの一部として新しいオペランドスタックが作成され、そこで操作が実行されます。 ここでの用語には注意が必要です。「スタック」という言葉は、呼び出しスタック、メソッド実行のコンテキストを提供するフレームのスタック、またはJVM命令が動作する特定のフレームのオペランドスタックを指す場合があります。

ローカル変数

各スタックフレームは、ローカル変数のテーブルを保持します。 This typically includes a reference to this object, any arguments that were passed when the method was called, and any local variables declared within the method body. Running javap with the -v option will include information about how each method's stack frame should be set up, including its local variable table:

 public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

In this example, there are two local variables. The variable in slot 0 is named this , with the type RegularPolygon . This is the reference to the method's own class. The variable in slot 1 is named sideLength , with the type D (indicating a double). This is the argument that is passed to our getPerimeter() method.

Instructions such as iload_1 , fstore_2 , or aload [n] , transfer different types of local variables between the operand stack and the local variable table. Since the first item in the table is usually the reference to this , the instruction aload_0 is commonly seen in any method that operates on its own class.

This concludes our walkthrough of JVM basics.

関連: Scalaマクロと準引用符を使用してボイラープレートコードを削減する