目次
Java Virtual Machineマシンは、Javaプログラミング言語をサポートするように設計されています。 OracleのJDKソフトウェアには、Javaプログラミング言語で記述されたソース・コードからJava Virtual Machineの命令セットまでのコンパイラと、Java Virtual Machine自体を実装するランタイム・システムが含まれています。 1つのコンパイラがJava Virtual Machineをどのように利用するかを理解することは、将来のコンパイラ・ライターおよびJava Virtual Machine自体を理解しようとするコンパイラにとって有用です。 この章の番号付きセクションは標準ではありません。
「コンパイラ」という用語は、Java Virtual Machineの命令セットから特定のCPUの命令セットへのトランスレータを参照する場合に使用されることがあります。 このようなトランスレータの一例はジャストインタイム(JIT)コード・ジェネレータで、Java Virtual Machineコードがロードされた後にのみプラットフォーム固有の命令を生成します。 この章では、コード生成に関連する問題に対処するものではなく、Javaプログラミング言語で記述されたソース・コードのJava Virtual Machine命令へのコンパイルに関連する問題のみについて説明します。
この章では、主にソース・コードの例と、OracleのJDKリリース1.0.2のjavacコンパイラによって生成されるJava Virtual Machineコードの注釈付きリストで構成されています。 Java Virtual Machineのコードは、JDKリリースとともに配布されるOracleのjavapユーティリティによる非公式の「仮想マシン・アセンブリ言語」出力で記述されます。 javapを使用して、コンパイル済メソッドの追加の例を生成できます。
例の形式は、アセンブリ・コードを読んだすべてのユーザーに理解しておく必要があります。 各命令は次の形式をとります。
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
<index>は、このメソッドのJava Virtual Machineコードのバイトを含む配列内の命令のopcodeのインデックスです。 あるいは、<index>は、メソッドの先頭からのバイト・オフセットと考えることもできます。 <opcode>は命令のopcodeのニーモニックで、0個以上の<operandN>は命令のオペランドです。 オプションの<comment>は、行末コメント構文で指定されます。
8 bipush 100 // Push int constant 100
コメント内の一部のマテリアルは、javapによって発行され、restは作成者によって提供されます。 各命令の前に配置される<index>は、制御転送命令のターゲットとして使用できます。 たとえば、goto 8命令は、索引8の命令に制御を移します。 Java Virtual Machine制御転送命令の実際のオペランドは、これらの命令のopcodesのアドレスからのオフセットであることに注意してください。これらのオペランドは、javapによって表示され(この章で説明します)、オフセットをより簡単にメソッドに読み込むことができます。
ここでは、ハッシュ記号を使用して実行時定数プール索引を表すオペランドを前に付け、次のように参照される実行時定数プール項目を識別するコメントによって指示に従います。
10 ldc #1 // Pushfloatconstant100.0
または
9 invokevirtual #4 // Method Example.addTwo(II)I
この章の目的上、オペランド・サイズなどの詳細を指定することは心配しません。
Java Virtual Machineのコードは、Java Virtual Machineの型設計および使用によって課される一般的な特性のセットを示します。 最初の例では、これらの多くに遭遇し、それらを詳細に検討します。
spinメソッドは、空のforループを100回スピンします。
void spin() {
int i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
コンパイラは、spinを次のようにコンパイルできます。
0 iconst_0 // Push int constant 0 1 istore_1 // Store into local variable 1 (i=0) 2 goto 8 // First time through don't increment 5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100) 14 return // Return void when done
Java Virtual Machineはスタック指向であり、ほとんどの操作はJava Virtual Machineの現在のフレームのオペランド・スタックから1つ以上のオペランドを取得するか、オペランド・スタックに結果をプッシュバックします。 メソッドが呼び出されるたびに新しいフレームが作成され、そのメソッドで使用する新しいオペランド・スタックとローカル変数のセット(§2.6)が作成されます。 計算の任意の時点で、多数のフレームが存在し、多数のネストされたメソッド呼出しに対応する、制御のスレッド当たりのオペランド・スタックも同様に多数存在する可能性があります。 現在のフレーム内のオペランドスタックのみがアクティブです。
Java Virtual Machineの命令セットは、様々なデータ型に対する操作に個別のバイトコードを使用することで、オペランドの型を区別します。 メソッドspinは、int型の値に対してのみ動作します。 型付きデータを操作するために選択されたコンパイル済みコード内の命令(iconst_0、istore_1、iinc、iload_1、if_icmplt)は、すべて int型に特化されています。
spinの2つの定数0および100は、2つの異なる命令を使用してオペランド・スタックにプッシュされます。 0は、iconst_<i>命令のファミリの1つであるiconst_0命令を使用してプッシュされます。 100は、即時のオペランドとしてプッシュする値をフェッチするbipush命令を使用してプッシュされます。
Java Virtual Machineでは、iconst_<i>、0、1、2、3、4および5 (iconst_<i>命令の場合)のオペランドをopcodeで暗黙的に作成することで、特定のオペランド(int定数)を利用することがよくあります。 iconst_0命令はint 0をプッシュすることを認識しているため、iconst_0は、プッシュする値を指示するためにオペランドを格納する必要はなく、オペランドをフェッチまたはデコードする必要もありません。 0のプッシュをbipush 0としてコンパイルすることは正しいことですが、spinのコンパイル・コードを1バイト長くしました。 単純な仮想マシンは、ループの周りのたびに明示的なオペランドのフェッチおよびデコードにさらに時間を費やしていました。 暗黙的なオペランドを使用すると、コンパイルされたコードがよりコンパクトで効率的になります。
spinのint iは、Java Virtual Machineのローカル変数1として格納されます。 ほとんどのJava Virtual Machine命令は、ローカル変数に対して直接ではなく、オペランド・スタックからポップされた値に対して動作するため、ローカル変数とオペランド・スタック間で値を転送する命令は、Java Virtual Machine用にコンパイルされたコードで一般的です。 これらの操作には、命令セットでの特別なサポートもあります。 spinでは、値はistore_1およびiload_1命令を使用してローカル変数との間で転送され、それぞれがローカル変数1に対して暗黙的に動作します。 istore_1命令は、オペランドスタックから intをポップし、ローカル変数 1に格納します。 iload_1命令は、ローカル変数 1の値をオペランドスタックにプッシュします。
ローカル変数の使用(および再利用)は、コンパイラ・ライターが行います。 特殊なロードおよび格納命令は、コンパイラ・ライターが可能なかぎりローカル変数を再利用するように促す必要があります。 結果のコードは、より高速でコンパクトになり、フレーム内の領域が少なくなります。
ローカル変数に対する特定の非常に頻繁な操作は、Java Virtual Machineによって特別にケータリングされます。 iinc命令は、ローカル変数の内容を1バイトの符号付きの値だけ増分します。 spinの iinc命令は、最初のローカル変数(最初のオペランド)を 1 (その2番目のオペランド)だけ増分します。 ループ構成を実装する場合、iinc命令は非常に便利です。
spinのforループは、主に次の手順で実行されます。
5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100)
bipush命令は、値 100を intとしてオペランドスタックにプッシュし、次に if_icmplt命令はその値をオペランドスタックからポップし、それを iと比較します。 比較が成功した場合(変数iが100より小さい場合)、制御は索引5に転送され、次のforループの反復が開始されます。 それ以外の場合、制御は if_icmpltのあとに命令に渡されます。
spinの例でループ・カウンタにint以外のデータ型を使用した場合、コンパイルされたコードは必ず異なるデータ型を反映するように変更されます。 たとえば、intのかわりにspinの例では、次のようにdoubleが使用されます。
void dspin() {
double i;
for (i = 0.0; i < 100.0; i++) {
; // Loop body is empty
}
}
コンパイルされたコードは次のとおりです。
Method void dspin() 0 dconst_0 // Push double constant 0.0 1 dstore_1 // Store into local variables 1 and 2 2 goto 9 // First time through don't increment 5 dload_1 // Push local variables 1 and 2 6 dconst_1 // Push double constant 1.0 7 dadd // Add; there is no dinc instruction 8 dstore_1 // Store result in local variables 1 and 2 9 dload_1 // Push local variables 1 and 2 10 ldc2_w #4 // Push double constant 100.0 13 dcmpg // There is no if_dcmplt instruction 14 iflt 5 // Compare and loop if less than (i < 100.0) 17 return // Return void when done
型付きデータを操作する命令は、double型に特化されるようになりました。 (ldc2_w命令については、この章の後半で説明します。)
double値は2つのローカル変数を占有しますが、アクセスされるのは2つのローカル変数の少ない索引のみであることを思い出してください。 これは、long型の値にも当てはまります。 例えば、
double doubleLocals(double d1, double d2) {
return d1 + d2;
}
これは次のようになります
Method double doubleLocals(double,double) 0 dload_1 // First argument in local variables 1 and 2 1 dload_3 // Second argument in local variables 3 and 4 2 dadd 3 dreturn
double値をdoubleLocalsに格納するために使用されるローカル変数ペアのローカル変数は、個別に操作しないでください。
Java Virtual MachineのOPコード・サイズが1バイトの場合、コンパイルされたコードは非常にコンパクトになります。 ただし、1バイトのopコードは、Java Virtual Machineの命令セットが小さいままであることも意味します。 妥協点として、Java Virtual Machineはすべてのデータ型に対して同等のサポートを提供していません。完全に直交しているわけではありません(表2.11.1-A)。
たとえば、例spinのfor文におけるint型の値の比較は、単一のif_icmplt命令を使用して実装できますが、Java Virtual Machine命令セットには、double型の値に対して条件分岐を実行する単一の命令はありません。 したがって、dspinは、dcmpg命令の後にiflt命令を指定して、double型の値の比較を実装する必要があります。
Java Virtual Machineは、int型のデータに対する最も直接的なサポートを提供します。 これは、Java Virtual Machineのオペランド・スタックおよびローカル変数配列の効率的な実装を一部想定しています。 また、一般的なプログラムのintデータの頻度によっても動機付けられます。 その他の整数型では、直接サポートが少なくなります。 たとえば、ストア、ロードまたは追加命令のbyte、charまたはshortバージョンはありません。 shortを使用して記述されたspinの例を次に示します。
void sspin() {
short i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
shortデータに対する操作の結果が適切な範囲内に収まるように、必要に応じてshort値とint値を変換する別のタイプ(おそらくint)で動作する命令を使用して、次のようにJava Virtual Machine用にコンパイルする必要があります。
Method void sspin() 0 iconst_0 1 istore_1 2 goto 10 5 iload_1 // The short is treated as though an int 6 iconst_1 7 iadd 8 i2s // Truncate int to short 9 istore_1 10 iload_1 11 bipush 100 13 if_icmplt 5 16 return
Java Virtual Machineでのbyte、charおよびshort型の直接サポートの欠如は、これらの型の値が内部的にintに昇格されるため、特に苦痛ではありません(byteおよびshortはintに符号拡張され、charはゼロ拡張です)。 したがって、byte、charおよびshortデータに対する操作は、int命令を使用して実行できます。 唯一の追加コストは、int操作の値を有効な範囲に切り捨てることです。
long型と浮動小数点型は、Java Virtual Machineの中間レベルのサポートがあり、条件付き制御転送命令の完全な補完しかありません。
Java Virtual Machineは、通常、オペランド・スタックで算術を行います。 (例外は、ローカル変数の値を直接増分するiinc命令です。) たとえば、align2grainメソッドでは、int値が指定された2の累乗に整列されます。
int align2grain(int i, int grain) {
return ((i + grain-1) & ~(grain-1));
}
算術演算のオペランドはオペランドスタックからポップされ、演算の結果はオペランドスタックにプッシュバックされます。 したがって、算術計算の結果は、ネスト計算のオペランドとして使用可能にできます。 たとえば、~(grain-1)の計算は、次の指示によって処理されます。
5 iload_2 // Push grain 6 iconst_1 // Push int constant 1 7 isub // Subtract; push result 8 iconst_m1 // Push int constant -1 9 ixor // Do XOR; push result
最初のgrain-1は、ローカル変数2および即時int値1の内容を使用して計算されます。 これらのオペランドは、オペランドスタックからポップされ、その差異がオペランドスタックに押し戻されます。 したがって、この違いはすぐにixor命令の1つのオペランドとして使用できます。 (~x == -1^xを思い出してください。) 同様に、ixor命令の結果は、後続のiand命令のオペランドになります。
メソッド全体のコードは次のとおりです。
Method int align2grain(int,int) 0 iload_1 1 iload_2 2 iadd 3 iconst_1 4 isub 5 iload_2 6 iconst_1 7 isub 8 iconst_m1 9 ixor 10 iand 11 ireturn
多くの数値定数、およびオブジェクト、フィールド、およびメソッドには、現在のクラスの実行時定数プールを介してアクセスされます。 オブジェクト・アクセスは後で考慮されます(§3.8)。 int型、long型、float型およびdouble型のデータと、クラスStringのインスタンスへの参照は、ldc、ldc_wおよびldc2_w命令を使用して管理されます。
ldcおよびldc_w命令は、doubleおよびlong以外の型の実行時定数プール(クラスStringのインスタンスを含む)の値にアクセスするために使用されます。 ldc_w命令は、ldcの代わりに使用され、実行時定数プール項目が多数あり、項目へのアクセスにさらに大きなインデックスが必要な場合にのみ使用されます。 ldc2_w命令は、doubleおよびlong型のすべての値にアクセスするために使用されます。ワイド以外のバリアントはありません。
byte、charまたはshort型の整数定数、および小さいint値は、bipush、sipushまたはiconst_<i>命令(§3.2)を使用してコンパイルできます。 fconst_<f>およびdconst_<d>命令を使用して、特定の小さい浮動小数点定数をコンパイルできます。
これらのいずれの場合も、コンパイルは単純です。 たとえば、次の定数です。
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
次のように設定されます。
Method void useManyNumeric() 0 bipush 100 // Push small int constant with bipush 2 istore_1 3 ldc #1 // Push large int constant (1000000) with ldc 5 istore_2 6 lconst_1 // A tiny long value uses small fast lconst_1 7 lstore_3 8 ldc2_w #6 // Push long 0xffffffff (that is, an int -1) // Any long constant value can be pushed with ldc2_w 11 lstore 5 13 ldc2_w #8 // Push double constant 2.200000 // Uncommon double values are also pushed with ldc2_w 16 dstore 7 ...do those calculations...
for文のコンパイルは、前の項(§3.2)で示しました。 Javaプログラミング言語のその他の制御構造(if-then-else、do、while、breakおよびcontinue)のほとんども、明らかな方法でコンパイルされます。 switch文のコンパイルは、例外のコンパイル(§3.12)およびfinally句のコンパイル(§3.13)と同様に、個別のセクション(§3.10)で処理されます。
さらに例として、whileループは明らかな方法でコンパイルされますが、Java Virtual Machineで使用できる特定の制御転送命令はデータ型によって異なります。 通常どおり、int型のデータはさらにサポートされます。次に例を示します。
void whileInt() {
int i = 0;
while (i < 100) {
i++;
}
}
次のようにコンパイルされます。
Method void whileInt() 0 iconst_0 1 istore_1 2 goto 8 5 iinc 1 1 8 iload_1 9 bipush 100 11 if_icmplt 5 14 return
while文のテスト(if_icmplt命令を使用して実装)は、ループのJava Virtual Machineコードの下部にあることに注意してください。 (これは、前述のspinの例でも同様です。) ループの下部にあるテストでは、ループの最初の反復の前にgoto命令を使用してテストに強制的に到達します。 そのテストが失敗し、ループ本体が入力されない場合、この余分な命令は無駄になります。 ただし、通常、whileループは、その本体が実行されると予想されるとき(多くの場合、反復が多いとき)に使用されます。 後続の反復では、ループの末尾にテストを置くと、ループを囲むたびにJava Virtual Machine命令が保存されます。テストがループの最上部にある場合、ループ本体の最上位に戻るには、後続のgoto命令が必要です。
他のデータ型を含む制御構造は、同様の方法でコンパイルされますが、それらのデータ型で使用可能な命令を使用する必要があります。 これにより、次のようなJava Virtual Machineの指示がさらに必要になるため、コードがやや効率が低下します。
void whileDouble() {
double i = 0.0;
while (i < 100.1) {
i++;
}
}
次のようにコンパイルされます。
Method void whileDouble() 0 dconst_0 1 dstore_1 2 goto 9 5 dload_1 6 dconst_1 7 dadd 8 dstore_1 9 dload_1 10 ldc2_w #4 // Push double constant 100.1 13 dcmpg // To compare and branch we have to use... 14 iflt 5 // ...two instructions 17 return
各浮動小数点型には、float型のfcmplとfcmpg、およびdouble型のdcmplとdcmpgの2つの比較命令があります。 変異体はNaNの治療においてのみ異なる。 NaNは順序付けられていない(§2.3.2)ため、いずれかのオペランドが NaNの場合、すべての浮動小数点比較が失敗します。 コンパイラは、非 NaN値で比較が失敗するか、NaNに遭遇するかに関係なく、同じ結果を生成する適切な型の比較命令のバリアントを選択します。 次に例を示します。
int lessThan100(double d) {
if (d < 100.0) {
return 1;
} else {
return -1;
}
}
コンパイル先:
Method int lessThan100(double) 0 dload_1 1 ldc2_w #4 // Push double constant 100.0 4 dcmpg // Push 1 if d is NaN or d > 100.0; // push 0 if d == 100.0 5 ifge 10 // Branch on 0 or 1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
dがNaNでなく、100.0より小さい場合、dcmpg命令はint -1をオペランド・スタックにプッシュし、ifge命令は分岐しません。 dが100.0より大きいか、NaNであるかにかかわらず、dcmpg命令はint 1をオペランド・スタックにプッシュし、ifge分岐もプッシュします。 dが100.0と等しい場合、dcmpg命令はint 0をオペランド・スタックにプッシュし、ifge分岐します。
dcmpl命令は、比較が逆になった場合も同じ効果を発揮します。
int greaterThan100(double d) {
if (d > 100.0) {
return 1;
} else {
return -1;
}
}
次のようになります。
Method int greaterThan100(double) 0 dload_1 1 ldc2_w #4 // Push double constant 100.0 4 dcmpl // Push -1 if d is NaN or d < 100.0; // push 0 if d == 100.0 5 ifle 10 // Branch on 0 or -1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
再度、比較がNaN以外の値で失敗するか、NaNが渡されたかに関係なく、dcmpl命令はオペランド・スタックにint値をプッシュし、それによってifleが分岐します。 両方の dcmp命令が存在しなかった場合、例のメソッドの1つが NaNを検出するためにより多くの作業を行う必要がありました。
n引数がインスタンス・メソッドに渡されると、慣例的に、新しいメソッド呼出し用に作成されたフレームの1からnの番号が付けられたローカル変数で受け取ります。 引数は、渡された順序で受信されます。 たとえば:
int addTwo(int i, int j) {
return i + j;
}
コンパイル先:
Method int addTwo(int,int) 0 iload_1 // Push value of local variable 1 (i) 1 iload_2 // Push value of local variable 2 (j) 2 iadd // Add; leave int result on operand stack 3 ireturn // Return int result
通常、インスタンス・メソッドは、ローカル変数0でreferenceをそのインスタンスに渡します。 Javaプログラミング言語では、インスタンスはthisキーワードを介してアクセスできます。
クラス(static)メソッドにはインスタンスがないため、ローカル変数0を使用する必要はありません。 クラス・メソッドは、索引0でローカル変数の使用を開始します。 addTwoメソッドがクラス・メソッドである場合、その引数は最初のバージョンと同様の方法で渡されます。
static int addTwoStatic(int i, int j) {
return i + j;
}
コンパイル先:
Method int addTwoStatic(int,int) 0 iload_0 1 iload_1 2 iadd 3 ireturn
唯一の違いは、メソッド引数が 1ではなくローカル変数 0で始まることです。
インスタンス・メソッドの通常のメソッド呼出しは、オブジェクトの実行時タイプにディスパッチされます。 (C++では仮想です。) このような呼出しは、invokevirtual命令を使用して実装されます。この命令は、その引数として、オブジェクトのクラス・タイプのバイナリ名の内部形式、起動するメソッドの名前、およびそのメソッドの記述子(§4.3.3)を示す実行時定数プール・エントリへの索引を取ります。 前にインスタンス・メソッドとして定義したaddTwoメソッドを起動するには、次のように記述します。
int add12and13() {
return addTwo(12, 13);
}
このコンパイル内容は次のとおりです。
Method int add12and13() 0 aload_0 // Push local variable 0 (this) 1 bipush 12 // Push int constant 12 3 bipush 13 // Push int constant 13 5 invokevirtual #4 // Method Example.addtwo(II)I 8 ireturn // Return int on top of operand stack; // it is the int result of addTwo()
この呼出しは、最初にreferenceを現在のインスタンスthisにオペランド・スタックにプッシュすることによって設定されます。 次に、メソッド呼出しの引数int値12および13がプッシュされます。 addTwoメソッドのフレームが作成されると、メソッドに渡される引数が新しいフレームのローカル変数の初期値になります。 つまり、thisのreferenceと、起動側によってオペランド・スタックにプッシュされる2つの引数は、呼び出されたメソッドのローカル変数0、1および2の初期値になります。
最後に、addTwoが呼び出されます。 戻ると、そのint戻り値は、呼出し元のフレームのオペランド・スタック(add12and13メソッド)にプッシュされます。 したがって、戻り値は、add12and13の起動元にすぐに返されるように配置されます。
add12and13からの戻り値は、add12and13のireturn命令によって処理されます。 ireturn命令は、現在のフレームのオペランド・スタックでaddTwoによって戻されたint値を取得し、呼出し元のフレームのオペランド・スタックにプッシュします。 次に、実行者に対して制御を戻し、実行者のフレームをカレントにします。 Java Virtual Machineは、数値データ型とreferenceデータ型の多くに対する個別の戻り命令、および戻り値のないメソッドに対するreturn命令を提供します。 メソッド呼出しのすべての種類に対して、同じ戻り命令セットが使用されます。
invokevirtual命令のオペランド(この例では、実行時定数プール索引#4)は、クラス・インスタンス内のメソッドのオフセットではありません。 コンパイラは、クラスインスタンスの内部レイアウトを認識しません。 かわりに、ランタイム定数プールに格納されるインスタンスのメソッドへのシンボリック参照を生成します。 これらの実行時定数プール・アイテムは、実行時に解決され、実際のメソッドの場所が決定されます。 同じことが、クラス・インスタンスにアクセスする他のすべてのJava Virtual Machine命令にも当てはまります。
addTwoのクラス(static)バリアントであるaddTwoStaticの起動は、次のように似ています。
int add12and13() {
return addTwoStatic(12, 13);
}
別のJava Virtual Machineメソッド呼出し命令が使用されます。
Method int add12and13() 0 bipush 12 2 bipush 13 4 invokestatic #3 // Method Example.addTwoStatic(II)I 7 ireturn
クラス(static)メソッドの呼出しのコンパイルは、インスタンス・メソッドの呼出しのコンパイルとよく似ていますが、thisは呼出し元によって渡されません。 したがって、メソッド引数は、ローカル変数 0 (§3.6)から受け取られます。 invokestatic命令は、常にクラス・メソッドの呼出しに使用されます。
インスタンス初期化メソッドを呼び出すには、invokespecial命令を使用する必要があります(§3.8)。 また、スーパークラス(super)でメソッドを呼び出す場合にも使用されます。 たとえば、次のように宣言されたクラスNearおよびFarがあります。
class Near {
int it;
int getItNear() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
メソッドFar.getItFar (スーパークラス・メソッドを呼び出す)は次のようになります。
Method int getItFar() 0 aload_0 1 invokespecial #4 // Method Near.getItNear()I 4 ireturn
invokespecial命令を使用して呼び出されたメソッドは、常にthisを最初の引数として呼び出されたメソッドに渡します。 通常どおり、ローカル変数 0で受信されます。
メソッド・ハンドルのターゲットを起動するには、コンパイラは実際の引数と戻り型を記録するメソッド・ディスクリプタを形成する必要があります。 コンパイラは、引数に対してメソッド呼出し変換を実行できません。かわりに、独自の変換されていない型に従って、それらをスタックにプッシュする必要があります。 コンパイラは、引数よりも前にスタックにプッシュされるメソッド・ハンドル・オブジェクトにreferenceを配置します。 コンパイラは、引数と戻り型を記述する記述子を参照するinvokevirtual命令を発行します。 メソッド解決の特別な配置(§5.4.3.3)によって、java.lang.invoke.MethodHandleのinvokeExactまたはinvokeメソッドを呼び出すinvokevirtual命令は、メソッド記述子が構文的に整形式であり、記述子で指定された型を解決できる場合に、常にリンクされます。
Java Virtual Machineクラス・インスタンスは、Java Virtual Machineの新しい命令を使用して作成されます。 Java Virtual Machineのレベルでは、コンストラクタはコンパイラ提供の名前が<init>のメソッドとして表示されることを思い出してください。 この特別に命名されたメソッドは、インスタンス初期化メソッド(§2.9)と呼ばれます。 1つのクラスに対して、複数のコンストラクタに対応する複数のインスタンス初期化メソッドが存在する場合があります。 クラス・インスタンスが作成され、そのインスタンス変数(クラスとそのすべてのスーパークラスを含む)がデフォルト値に初期化されると、新しいクラス・インスタンスのインスタンス初期化メソッドが呼び出されます。 たとえば:
Object create() {
return new Object();
}
コンパイル先:
Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn
クラス・インスタンスは、数値とほぼ同じように(reference型として)渡され、戻されます。ただし、型referenceには、次のような独自の命令が補完されます。
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}
}
次のようになります。
Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn
クラス・インスタンス(インスタンス変数)のフィールドには、getfieldおよびputfield命令を使用してアクセスします。 iがint型のインスタンス変数である場合、メソッドsetItおよびgetItは次のように定義されます。
void setIt(int value) {
i = value;
}
int getIt() {
return i;
}
次になります。
Method void setIt(int) 0 aload_0 1 iload_1 2 putfield #4 // Field Example.i I 5 return Method int getIt() 0 aload_0 1 getfield #4 // Field Example.i I 4 ireturn
メソッド起動命令のオペランドと同様に、putfieldおよびgetfield命令(実行時定数プール索引#4)のオペランドは、クラス・インスタンスのフィールドのオフセットではありません。 コンパイラは、実行時定数プールに格納されるインスタンスのフィールドへのシンボリック参照を生成します。 これらの実行時定数プール項目は実行時に解決され、参照オブジェクト内のフィールドの場所が決定されます。
Java Virtual Machine配列もオブジェクトです。 配列は、個別の命令セットを使用して作成および操作されます。 newarray命令は、数値型の配列を作成するために使用されます。 コード:
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
次のようにコンパイルできます。
Method void createBuffer() 0 bipush 100 // Push int constant 100 (bufsz) 2 istore_2 // Store bufsz in local variable 2 3 bipush 12 // Push int constant 12 (value) 5 istore_3 // Store value in local variable 3 6 iload_2 // Push bufsz... 7 newarray int // ...and create new int array of that length 9 astore_1 // Store new array in buffer 10 aload_1 // Push buffer 11 bipush 10 // Push int constant 10 13 iload_3 // Push value 14 iastore // Store value at buffer[10] 15 aload_1 // Push buffer 16 bipush 11 // Push int constant 11 18 iaload // Push value at buffer[11]... 19 istore_3 // ...and store it in value 20 return
anewarray命令は、オブジェクト参照の1次元配列を作成するために使用されます。次に例を示します。
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
次のようになります。
Method void createThreadArray()
0 bipush 10 // Push int constant 10
2 istore_2 // Initialize count to that
3 iload_2 // Push count, used by anewarray
4 anewarray class #1 // Create new array of class Thread
7 astore_1 // Store new array in threads
8 aload_1 // Push value of threads
9 iconst_0 // Push int constant 0
10 new #1 // Create instance of class Thread
13 dup // Make duplicate reference...
14 invokespecial #5 // ...for Thread's constructor
// Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return
anewarray命令は、多次元配列の最初のディメンションの作成にも使用できます。 または、multianewarray命令を使用して、複数のディメンションを一度に作成できます。 たとえば、次の3次元配列があります。
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
作成者:
Method int create3DArray()[][][] 0 bipush 10 // Push int 10 (dimension one) 2 iconst_5 // Push int 5 (dimension two) 3 multianewarray #1 dim #2 // Class [[[I, a three-dimensional // int array; only create the // first two dimensions 7 astore_1 // Store new array... 8 aload_1 // ...then prepare to return it 9 areturn
multianewarray命令の最初のオペランドは、作成する配列クラス・タイプに対する実行時定数プール索引です。 2つ目は、実際に作成する配列型のディメンションの数です。 multianewarray命令を使用すると、create3DArrayのコードが示すように、型のすべてのディメンションを作成できます。 多次元配列は単なるオブジェクトであるため、aload_1およびareturn命令によってそれぞれロードおよび返されます。 配列クラス名の詳細は、§4.4.1を参照してください。
すべての配列には、arraylength命令を介してアクセスされる長さが関連付けられています。
switch文をコンパイルするには、tableswitchおよびlookupswitch命令を使用します。 tableswitch命令は、switchのケースをターゲット・オフセット表への索引として効率的に表すことができる場合に使用されます。 switchのdefaultターゲットは、switchの式の値が有効なインデックスの範囲外にある場合に使用されます。 次に例を示します。
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
コンパイル先:
Method int chooseNear(int) 0 iload_1 // Push local variable 1 (argument i) 1 tableswitch 0 to 2: // Valid indices are 0 through 2 0: 28 // If i is 0, continue at 28 1: 30 // If i is 1, continue at 30 2: 32 // If i is 2, continue at 32 default:34 // Otherwise, continue at 34 28 iconst_0 // i was 0; push int constant 0... 29 ireturn // ...and return it 30 iconst_1 // i was 1; push int constant 1... 31 ireturn // ...and return it 32 iconst_2 // i was 2; push int constant 2... 33 ireturn // ...and return it 34 iconst_m1 // otherwise push int constant -1... 35 ireturn // ...and return it
Java Virtual Machineのtableswitchおよびlookupswitch命令は、intデータに対してのみ動作します。 byte、charまたはshort値に対する操作は内部的にintにプロモートされるため、これらの型のいずれかに評価される式を持つswitchは、int型と評価されるかのようにコンパイルされます。 chooseNearメソッドがshort型を使用して記述されている場合は、int型を使用する場合と同じJava Virtual Machine命令が生成されます。 switchで使用するには、他の数値型をint型に絞り込む必要があります。
switchのケースが疎の場合、tableswitch命令の表表現は、領域に関して非効率になります。 かわりに、lookupswitch命令を使用できます。 lookupswitch命令は、intキー(caseラベルの値)と表内のターゲット・オフセットをペアにします。 lookupswitch命令が実行されると、switchの式の値が表内のキーと比較されます。 いずれかのキーが式の値と一致する場合、関連するターゲット・オフセットで実行が続行されます。 一致するキーがない場合、defaultターゲットで実行が続行されます。 たとえば、コンパイルされたコードは次のとおりです。
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
lookupswitch命令を除き、chooseNearのコードと同様です。
Method int chooseFar(int) 0 iload_1 1 lookupswitch 3: -100: 36 0: 38 100: 40 default: 42 36 iconst_m1 37 ireturn 38 iconst_0 39 ireturn 40 iconst_1 41 ireturn 42 iconst_m1 43 ireturn
Java Virtual Machineでは、lookupswitch命令の表をキーでソートして、実装でリニア・スキャンよりも効率的に検索を使用できるようにすることが指定されています。 それでも、lookupswitch命令は、単にtableswitchのような表への境界チェックおよび索引を実行するのではなく、そのキーで一致を検索する必要があります。 したがって、tableswitch命令は、領域を考慮して選択できるlookupswitchよりも効率的です。
Java Virtual Machineには、オペランド・スタックの内容を型なしの値として操作する命令の大部分が含まれています。 これらは、Java Virtual Machineがオペランド・スタックのdeft操作に依存しているために役立ちます。 次に例を示します。
public long nextIndex() {
return index++;
}
private long index = 0;
次のようにコンパイルされます。
Method long nextIndex() 0 aload_0 // Push this 1 dup // Make a copy of it 2 getfield #4 // One of the copies of this is consumed // pushing long field index, // above the original this 5 dup2_x1 // The long on top of the operand stack is // inserted into the operand stack below the // original this 6 lconst_1 // Push long constant 1 7 ladd // The index value is incremented... 8 putfield #4 // ...and the result stored in the field 11 lreturn // The original value of index is on top of // the operand stack, ready to be returned
Java Virtual Machineでは、オペランド・スタック操作命令がオペランド・スタック上の個々の値を変更または分割することは許可されません。
例外は、throwキーワードを使用してプログラムからスローされます。 そのコンパイルは簡単です:
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}
}
次のようになります。
Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to its constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc
try-catchコンストラクトのコンパイルは簡単です。 たとえば:
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}
次のようにコンパイルされます。
Method void catchOne() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: // Example.handleExc(LTestExc;)V 11 return // Return after handling TestExc Exception table: From To Target Type 0 4 5 Class TestExc
より詳しく見ると、tryブロックは、tryが存在しない場合と同様にコンパイルされます。
Method void catchOne() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 return // End of try block; normal return
tryブロックの実行中に例外がスローされない場合、tryが存在しないかのように動作します。つまり、tryItOutが呼び出され、catchOneが戻されます。
tryブロックの後には、単一のcatch句を実装するJava Virtual Machineコードがあります。
5 astore_1 // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: // Example.handleExc(LTestExc;)V 11 return // Return after handling TestExc Exception table: From To Target Type 0 4 5 Class TestExc
handleExcの呼出し(catch句の内容)も、通常のメソッド呼出しのようにコンパイルされます。 ただし、catch句が存在すると、コンパイラは例外表エントリ(§2.10、§4.7.3)を生成します。 catchOneメソッドの例外表には、catchOneのcatch句が処理できる1つの引数(クラスTestExcのインスタンス)に対応するエントリが1つあります。 catchOneのインデックス0と4の間の命令の実行中にTestExcのインスタンスである値がスローされた場合、catch句のブロックを実装する索引5のJava Virtual Machineコードに制御が転送されます。 スローされる値がTestExcのインスタンスではない場合、catchOneのcatch句はそれを処理できません。 かわりに、値はcatchOneの起動元に再スローされます。
tryには、複数のcatch句を含めることができます。
void catchTwo() {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc(e);
} catch (TestExc2 e) {
handleExc(e);
}
}
指定されたtry文の複数のcatch句は、次に示すように、catch句ごとにJava Virtual Machineコードを1つずつ追加し、例外表にエントリを追加することによってコンパイルされます。
Method void catchTwo() 0 aload_0 // Begin try block 1 invokevirtual #5 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Beginning of handler for TestExc1; // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: // Example.handleExc(LTestExc1;)V 11 return // Return after handling TestExc1 12 astore_1 // Beginning of handler for TestExc2; // Store thrown value in local var 1 13 aload_0 // Push this 14 aload_1 // Push thrown value 15 invokevirtual #7 // Invoke handler method: // Example.handleExc(LTestExc2;)V 18 return // Return after handling TestExc2 Exception table: From To Target Type 0 4 5 Class TestExc1 0 4 12 Class TestExc2
try句(索引0と4の間)の実行中に、1つ以上のcatch句のパラメータ(値が1つ以上のパラメータのインスタンス)と一致する値がスローされた場合、このようなcatch句の最初の(最も内側の)が選択されます。 制御は、そのcatch句のブロックのJava Virtual Machineコードに転送されます。 スローされた値がcatchTwoのcatch句のいずれのパラメータとも一致しない場合、Java Virtual Machineは、catchTwoのcatch句でコードを起動せずに値を再スローします。
ネストされたtry-catch文は、複数のcatch句を含むtry文とほぼ同じようにコンパイルされます。
void nestedCatch() {
try {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc1(e);
}
} catch (TestExc2 e) {
handleExc2(e);
}
}
次のようになります。
Method void nestedCatch() 0 aload_0 // Begin try block 1 invokevirtual #8 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Beginning of handler for TestExc1; // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: // Example.handleExc1(LTestExc1;)V 11 return // Return after handling TestExc1 12 astore_1 // Beginning of handler for TestExc2; // Store thrown value in local var 1 13 aload_0 // Push this 14 aload_1 // Push thrown value 15 invokevirtual #6 // Invoke handler method: // Example.handleExc2(LTestExc2;)V 18 return // Return after handling TestExc2 Exception table: From To Target Type 0 4 5 Class TestExc1 0 12 12 Class TestExc2
catch句のネストは、例外表でのみ表されます。 Java Virtual Machineでは、例外表エントリのネストや順序付けは強制されません(§2.10)。 ただし、try-catchコンストラクトは構造化されているため、コンパイラは例外ハンドラ表のエントリを常に順序付けできるため、スローされた例外とそのメソッド内のプログラム・カウンタ値について、スローされた例外に一致する最初の例外ハンドラが、最も内側の一致するcatch句に対応するようになります。
たとえば、tryItOutの呼出し(索引1)でTestExc1インスタンスがスローされた場合、handleExc1を起動するcatch句によって処理されます。 これは、例外が外部のcatch句(TestExc2の捕捉)の範囲内で発生し、外部のcatch句がスローされた値を処理できた場合でも同様です。
微妙な点として、catch句の範囲は「開始」の端に包含され、「終了」の端に排他的であることに注意してください(§4.7.3)。 したがって、TestExc1をキャッチするcatch句の例外表エントリは、オフセット4でのreturn命令をカバーしません。 ただし、TestExc2をキャッチするcatch句の例外表エントリは、オフセット11でのreturn命令をカバーしています。 ネストされたcatch句内の戻り命令は、catch句のネストによってカバーされる命令の範囲に含まれます。
finallyのコンパイル (この項では、コンパイラがバージョン番号50.0以下のclassファイルを生成して、jsr命令を使用できるようにすることを前提としています。 §4.10.2.5も参照してください。)
try-finally文のコンパイルは、try-catchのコンパイルと似ています。 try文の外部で制御を転送する前に、その転送が正常か異常かに関係なく、例外がスローされたため、最初にfinally句を実行する必要があります。 この単純な例の場合:
void tryFinally() {
try {
tryItOut();
} finally {
wrapItUp();
}
}
コンパイルされたコードは次のとおりです。
Method void tryFinally() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 jsr 14 // Call finally block 7 return // End of try block 8 astore_1 // Beginning of handler for any throw 9 jsr 14 // Call finally block 12 aload_1 // Push thrown value 13 athrow // ...and rethrow value to the invoker 14 astore_2 // Beginning of finally block 15 aload_0 // Push this 16 invokevirtual #5 // Method Example.wrapItUp()V 19 ret 2 // Return from finally block Exception table: From To Target Type 0 4 8 any
try文の外部で制御を渡す方法には、そのブロックの一番下を落とす方法、戻す方法、break文またはcontinue文の実行方法、または例外の呼出し方法の4つがあります。 tryItOutが例外を発生させずに戻ると、jsr命令を使用して制御がfinallyブロックに転送されます。 インデックス 4の jsr 14命令は、インデックス 14の finallyブロックのコードを「サブルーチン呼び出し」します(finallyブロックは組み込みサブルーチンとしてコンパイルされます)。 finallyブロックが完了すると、ret 2命令は、インデックス 4の jsr命令に続く命令に制御を返します。
さらに詳しく、サブルーチン呼び出しは次のように動作します。jsr命令は、次の命令のアドレス(インデックス 7の return)をジャンプする前にオペランドスタックにプッシュします。 ジャンプ・ターゲットであるastore_2命令は、オペランド・スタック上のアドレスをローカル変数2に格納します。 finallyブロックのコード(この場合は、aload_0およびinvokevirtual命令)が実行されます。 そのコードが正常に実行されると、ret命令はローカル変数 2からアドレスを取得し、そのアドレスでの実行を再開します。 return命令が実行され、tryFinallyは正常に戻ります。
finally句を含むtry文は、try文内でスローされた例外を処理できる特別な例外ハンドラを持つようにコンパイルされます。 tryItOutが例外をスローすると、tryFinallyの例外表で適切な例外ハンドラが検索されます。 特殊ハンドラが見つかったため、索引8で実行が続行されます。 インデックス 8の astore_1命令は、スローされた値をローカル変数 1に格納します。 次のjsr命令は、finallyブロックのコードに対するサブルーチン・コールを実行します。 コードが正常に返されると、インデックス 12の aload_1命令はスローされた値をオペランドスタックに戻し、次の athrow命令はその値を再スローします。
catch句とfinally句の両方を使用してtry文をコンパイルすると、より複雑になります。
void tryCatchFinally() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
} finally {
wrapItUp();
}
}
次のようになります。
Method void tryCatchFinally() 0 aload_0 // Beginning of try block 1 invokevirtual #4 // Method Example.tryItOut()V 4 goto 16 // Jump to finally block 7 astore_3 // Beginning of handler for TestExc; // Store thrown value in local var 3 8 aload_0 // Push this 9 aload_3 // Push thrown value 10 invokevirtual #6 // Invoke handler method: // Example.handleExc(LTestExc;)V 13 goto 16 // This goto is unnecessary, but was // generated by javac in JDK 1.0.2 16 jsr 26 // Call finally block 19 return // Return after handling TestExc 20 astore_1 // Beginning of handler for exceptions // other than TestExc, or exceptions // thrown while handling TestExc 21 jsr 26 // Call finally block 24 aload_1 // Push thrown value... 25 athrow // ...and rethrow value to the invoker 26 astore_2 // Beginning of finally block 27 aload_0 // Push this 28 invokevirtual #5 // Method Example.wrapItUp()V 31 ret 2 // Return from finally block Exception table: From To Target Type 0 4 7 Class TestExc 0 16 20 any
try文が正常に完了すると、索引4のgoto命令は、索引16のfinallyブロックに対するサブルーチン・コールにジャンプします。 索引26のfinallyブロックが実行され、制御が索引19のreturn命令に戻り、tryCatchFinallyが正常に戻されます。
tryItOutがTestExcのインスタンスをスローすると、例外を処理するために、例外表内の最初の(最も内側の)適用可能な例外ハンドラが選択されます。 その例外ハンドラのコードは、索引7から始まり、スローされた値をhandleExcに渡し、その戻り時に索引26のfinallyブロックに対して通常のケースと同じサブルーチン・コールを実行します。 handleExcによって例外がスローされない場合、tryCatchFinallyは通常どおり戻されます。
tryItOutがTestExcのインスタンスではない値をスローした場合、またはhandleExc自体が例外をスローした場合、条件は例外表の2番目のエントリによって処理され、索引0と16の間でスローされるすべての値が処理されます。 この例外ハンドラは、最初にスローされた値がローカル変数1に格納される索引20に制御を転送します。 索引26のfinallyブロックのコードは、サブルーチンと呼ばれます。 戻り値の場合、スローされた値はローカル変数1から取得され、athrow命令を使用して再スローされます。 finally句の実行中に新しい値がスローされた場合、finally句は異常終了し、tryCatchFinallyは突然戻り、新しい値をその呼出し元にスローします。
Java Virtual Machineでの同期は、モニターのエントリと終了によって(monitorenterおよびmonitorexit命令を使用して)明示的に、または暗黙的に(メソッドの起動と戻り命令によって)実装されます。
Javaプログラミング言語で記述されたコードの場合、同期の最も一般的な形式はsynchronizedメソッドです。 synchronizedメソッドは、通常、monitorenterおよびmonitorexitを使用して実装されません。 かわりに、実行時定数プールでは、メソッド呼出し命令(§2.11.10)によってチェックされるACC_SYNCHRONIZEDフラグによって単純に区別されます。
monitorenterおよびmonitorexit命令により、synchronized文をコンパイルできます。 たとえば:
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
次のようにコンパイルされます。
Method void onlyMe(Foo) 0 aload_1 // Push f 1 dup // Duplicate it on the stack 2 astore_2 // Store duplicate in local variable 2 3 monitorenter // Enter the monitor associated with f 4 aload_0 // Holding the monitor, pass this and... 5 invokevirtual #5 // ...call Example.doSomething()V 8 aload_2 // Push local variable 2 (f) 9 monitorexit // Exit the monitor associated with f 10 goto 18 // Complete the method normally 13 astore_3 // In case of any throw, end up here 14 aload_2 // Push local variable 2 (f) 15 monitorexit // Be sure to exit the monitor! 16 aload_3 // Push thrown value... 17 athrow // ...and rethrow value to the invoker 18 return // Return in the normal case Exception table: From To Target Type 4 10 13 any 13 16 13 any
コンパイラは、メソッド呼出しの完了時に、メソッド呼出し以降に実行されるmonitorexit命令ごとにmonitorenter命令が実行されるようにします。 これは、メソッド呼出しが正常に完了したか(§2.6.4)、突然完了したか(§2.6.5)。 monitorenter命令とmonitorexit命令の適切なペアリングを強制的に強制するために、コンパイラは例外に一致し、関連するコードで必要なmonitorexit命令を実行する例外ハンドラ(§2.10)を生成します。
classファイルの注釈の表現については、§4.7.16-§4.7.22で説明します。 これらのセクションでは、クラス、インタフェース、フィールド、メソッド、メソッド・パラメータおよび型パラメータの宣言の注釈、およびそれらの宣言で使用される型のアノテーションの表現方法を明確にしています。 パッケージ宣言の注釈には、ここで示す追加のルールが必要です。
コンパイラは、実行時に使用可能にする必要がある注釈付きパッケージ宣言を検出すると、次のプロパティを持つclassファイルを発行します。
classファイルは、インタフェースを表します。つまり、ClassFile構造のACC_INTERFACEおよびACC_ABSTRACTフラグが設定されます(§4.1)。
classファイルのバージョン番号が50.0より小さい場合、ACC_SYNTHETICフラグは設定解除されます。classファイルのバージョン番号が50.0以上である場合、ACC_SYNTHETICフラグが設定されます。
このインタフェースはパッケージ・アクセス(JLS§6.6.1)を持っています。
インタフェースの名前は、package-name.package-infoの内部形式(§4.2.1)です。
このインタフェースにはスーパーインタフェースがありません。
インタフェースのメンバーは、The Java Language Specification、 Java SE 26 Edition (JLS§9.2)によって示されるメンバーのみです。
パッケージ宣言の注釈は、ClassFile構造のattributes表にRuntimeVisibleAnnotationsおよびRuntimeInvisibleAnnotations属性として格納されます。
モジュール宣言(JLS§7.7)を含むコンパイル・ユニットは、Module属性を含むclassファイルにコンパイルされます。
通常、モジュール宣言を含むコンパイル・ユニットの名前はmodule-info.javaで、パッケージ宣言のみを含むコンパイル・ユニットのpackage-info.java規則をエコーします。 したがって、慣例上、モジュール宣言のコンパイル形式の名前はmodule-info.classです。
ClassFile構造のaccess_flags項目のフラグACC_MODULE (0x8000)は、このclassファイルがモジュールを宣言することを示します。 ACC_MODULEは、ACC_ANNOTATION (0x2000)およびACC_ENUM (0x4000)と同様の役割を担い、このclassファイルに「通常のクラスではない」フラグを付けます。 ACC_MODULEでは、クラスまたはインタフェースのアクセシビリティは記述されません。
Module属性は、モジュールの依存性について明示的であり、ClassFileレベルには暗黙的なrequiresディレクティブはありません。 requires_count項目が0(ゼロ)の場合、Java SEプラットフォームでは、requires表またはその中の特定のエントリが存在することを推測しません。java.baseは、requires_countがゼロである唯一のモジュールです。これは、初期モジュールであるためです。 他のすべてのモジュールはjava.baseに依存するため、Module属性には少なくとも1つの長さのrequires表が必要です。 コンパイル・ユニットに、java.baseへの依存を明示的に示さないモジュール宣言(java.baseを除く)が含まれている場合、コンパイラはrequires表のjava.baseのエントリを発行し、暗黙的に宣言されたことを示すACC_MANDATEDとしてフラグを付ける必要があります。
カプセル化の場合、Module属性は、通常のモジュールによってエクスポートおよびオープンされるパッケージについて明示的です。通常のモジュールのClassFileレベルには、暗黙的なexportsまたはopensディレクティブはありません。 exports_count項目またはopens_count項目が0(ゼロ)の場合、Java SEプラットフォームでは、exports表またはopens表、およびその中の特定のエントリの存在は推測されません。 一方、オープン・モジュールの場合、Module属性はモジュールによってオープンされたパッケージに対して暗黙的です。 opens_count項目がゼロの場合でも、オープン・モジュールのすべてのパッケージが他のすべてのモジュールに対してオープンされます。
Module属性は、モジュールのサービスの消費と提供について明示的です。ClassFileレベルには暗黙的なusesまたはprovidesディレクティブはありません。