: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

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

; 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

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

: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

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

: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

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

: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

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

その状態を追跡します。メソッドm4()このフィールドの値が0であるかどうかをチェックし、m4であることを示します。まだ初期化されていません。この場合、m4$lzycompute()が呼び出され、m4が入力されますそしてその値を返します。このプライベートメソッドは、bitmap

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

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

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

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



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

前提条件

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

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

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

そしてもちろん、例に沿ってフォローしたい読者には、Scalaコンパイラの実用的なインストールが必要です。この記事はを使用して書かれました スケール2.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クラスの各フィールドについて、フィールドとそのゲッターメソッドが生成されていることがわかります。フィールドはプライベートでファイナルですが、メソッドはパブリックです。

valを置き換える場合varPersonでソースを作成して再コンパイルしてから、フィールドの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.'':()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が続きます。これは、このメソッドの結果を、インデックスm1によって参照されるフィールド#22に割り当てます。定数プールで。 2回目の呼び出しの後に同じパターンが続きます。今回は、m2でインデックス付けされたフィールド#24に値を割り当てます。定数プールで。

つまり、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の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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.'':()V 11: invokespecial #22 // Method Printer.'':(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を見てみましょう。 ScalaのFunction1を拡張していることがわかります(AbstractFunction1として)apply()を実装する方法。実際には、2つのapply()を作成しますメソッドは、一方が他方をラップし、一緒に型チェックを実行し(この場合、入力がStringであること)、無名関数を実行します(入力をprintln()で出力します)。

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

各命令は1バイトで始まります オペコード 特定の命令の形式に応じて、JVM命令を識別し、その後に操作対象の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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。

の値も設定します次回はm4()が呼び出されると、初期化メソッドの呼び出しがスキップされ、代わりにm4の値が返されます。

Scalaレイジー値への最初の呼び出しの結果。

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

遅延値の状態を示すために必要なビットは1つだけです。したがって、レイジー値が32個以下の場合、1つの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クラスには、タイプoutputの1つのフィールドString => Unitがあります。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 7: dup 8: invokespecial #19 // Method Hello$$anonfun.'':()V 11: invokespecial #22 // Method Printer.'':(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を含むs => println(s)を作成します。 Printerこのオブジェクトでoutputとして初期化されますフィールド。次に、このフィールドはスタックにロードされ、オペランド'Hello'で実行されます。

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

public final class Hello$$anonfun extends scala.runtime.AbstractFunction1 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 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

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

したがって、このエレガントな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コンパイラがそのような呼び出しに遭遇すると、次のようになります。

したがって、トレイトの強力な概念が、大きなオーバーヘッドを引き起こさない方法で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 '':()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.'':()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

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

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

結論

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

テーブル内のデータが冗長になった場合:

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


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

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

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

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

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

Javaにはjavapが付属しています.classを逆コンパイルするコマンドラインユーティリティ人間が読める形式にファイルします。 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を追加しましょう。オプション:

最高の有料出会い系サイト2015
$ 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.'':()V 9: return }

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

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

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

コンスタントプール

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

クラスファイル形式の仕様には、ファイルの「 定数プール 、リンカが必要とするすべての参照の表。さまざまなタイプのエントリが含まれています。

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

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

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

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のエントリです。 、およびタイプ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は、独自の内部命令セットを使用して、コンパイルされたコードを実行します。実行中javap -cでオプションは、コンパイルされたメソッドの実装を出力に含めます。 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 ...

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

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

以下では、いくつかの一般的な手順について説明します。完全なJVM命令セットの詳細については、 ドキュメンテーション

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

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

ccorpとscorpの違いは何ですか

スタックフレームには、いくつかの異なる構造が含まれています。 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命令は、オペランドスタックを操作して、複雑な機能を実行できます。

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

ローカル変数

各スタックフレームは、 ローカル変数 。これには通常、thisへの参照が含まれますオブジェクト、メソッドが呼び出されたときに渡された引数、およびメソッド本体内で宣言されたローカル変数。実行中javap -vでオプションには、ローカル変数テーブルなど、各メソッドのスタックフレームの設定方法に関する情報が含まれます。

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

この例では、2つのローカル変数があります。スロット0の変数の名前はthisで、タイプはRegularPolygonです。これは、メソッド自体のクラスへの参照です。スロット1の変数の名前はsideLengthで、タイプはDです。 (ダブルを示します)。これは、getPerimeter()に渡される引数です。方法。

iload_1fstore_2aload [n]などの命令は、オペランドスタックとローカル変数テーブルの間でさまざまなタイプのローカル変数を転送します。通常、表の最初の項目はthisへの参照であるため、命令aload_0独自のクラスで動作するすべてのメソッドで一般的に見られます。

これで、JVMの基本についてのウォークスルーは終わりです。 メインの記事に戻るには、ここをクリックしてください。