Java を勉強している友人は皆、Java が当初から「一度書けばどこでも実行できる」というプラットフォームの独立性を掲げてきたことを知っているはずです。実際、Java プラットフォームには、言語の独立性というもう 1 つの無関係性があります。 . 、言語の独立性を実現するには、Java システムのクラスのファイル構造、またはバイトコードであるということが非常に重要ですが、実際には Java には最初から 2 つの仕様があり、1 つは Java 言語仕様で、もう 1 つは Java 仮想マシン仕様です。Java 言語仕様は制約のみを規定しています。 Java 言語とルールに関連しており、仮想マシンの仕様は真にクロスプラットフォームの観点から設計されています。今日は、Java のクラス ファイルに対応するバイトコードがどのようなものであるかを確認するための実践的な例を取り上げます。 この記事では、まず Class がどのような内容で構成されているかを大まかに説明し、次に実際の Java クラスを使用してクラスのファイル構造を解析します。
続行する前に、まず次の点を明確にする必要があります。
1) クラス ファイルは 8 バイト単位のバイト ストリームで構成され、これらのバイト ストリームは指定された順序で厳密に配置され、8 バイトを超えるファイルの場合、データはビッグ エンディアン順に格納されます。つまり、上位バイトは下位アドレスに格納され、下位バイトは上位アドレスに格納されます。これは、クロスプラットフォームのクラス ファイルにとっても重要です。PowerPC アーキテクチャではビッグ エンディアンのストレージ順序が使用されるのに対し、x86 シリーズ プロセッサではリトル エンディアンのストレージ順序が使用されるため、各プロセッサ アーキテクチャでクラス ファイルを維持するには、統合ストレージが必要です。そのためには、仮想マシンの仕様を統一する必要があります。
2) クラス ファイル構造は、C 言語に似た構造を使用してデータを格納します。データ項目には、符号なし数値とテーブルの 2 つの主なタイプがあります。符号なし数値は、数値、インデックス参照、文字列 (u1、u2 など) を表現するために使用されます。 、u4、u8 はそれぞれ 1 バイト、2 バイト、4 バイト、8 バイトの符号なし数値を表し、テーブルは複数の符号なし数値と他のテーブルから構成される複合構造です。おそらくここにいる人は、符号なしの数値やテーブルが何なのかよくわかっていないかもしれませんが、以下に例を挙げて説明しますので、問題はありません。
上記の 2 つの点を明確にした後、クラス ファイル内で厳密な順序で配置されたバイト ストリームに含まれる特定のデータを見てみましょう。
上の図を見ると、注意が必要な点が 1 つあります。cp_info は定数プールを表しており、上の図では constant_pool[constant_pool_count-1] が constant_pool_co で定数プールを表すために使用されています。 unt-1 定数、ここでは配列形式で表現していますが、すべての定数プールの定数長が同じであると誤解しないでください。実際には、ここでは説明の便宜上、配列メソッドを使用しています。プログラミング言語では、int 型の配列は各 int の長さが同じではありません。この点を明確にした上で、上の図の各項目が具体的に何を表しているのかを振り返ってみましょう。
1) u4 マジックはマジックナンバーを表し、マジックナンバーは 4 バイトを占めます。マジックナンバーは何をしますか?これは実際には、ファイル タイプが JPG 画像や AVI ムービーではなく、Class ファイルであることを意味します。 Class ファイルに対応するマジックナンバーは 0xCAFEBABE です。
2) u2minor_version はクラス ファイルのマイナー バージョン番号を表し、このバージョン番号は u2 型の符号なし数値表現です。
3) u2 Major_version はクラス ファイルのメジャー バージョン番号を表し、メジャー バージョン番号は u2 型の符号なし数値表現です。 Major_version とminor_version は主に、現在の仮想マシンがクラス ファイルの現在のバージョンを受け入れるかどうかを示すために使用されます。 Java コンパイラのバージョンによってコンパイルされるクラス ファイルのバージョンは異なります。仮想マシンの上位バージョンは、下位バージョンのコンパイラによってコンパイルされたクラス ファイル構造をサポートします。たとえば、Java SE 6.0 に対応する仮想マシンは、Java SE 5.0 コンパイラによってコンパイルされたクラス ファイル構造をサポートしますが、その逆はサポートしません。
4) u2 constant_pool_count は、定数プールの数を表します。ここでは、定数プールが何であるかに注目する必要があります。Jvm メモリ モデルの実行時定数プールと混同しないでください。クラス ファイルの定数プールには、主にリテラルとシンボル参照が格納されます。最終定数の値、またはまたは、特定の属性の初期値などですが、シンボル参照には主にクラスとインターフェイス、フィールド名と記述子、メソッド名と記述子の完全修飾名が格納されます。ここでの名前は、誰にとっても理解しやすいものです。記述子の概念については、後でフィールド テーブルとメソッド テーブルについて説明するときに説明します。また、Jvmのメモリモデルはヒープ、スタック、メソッド領域、プログラムカウンタで構成されているのは皆さんご存知で、メソッド領域にはランタイム定数プールと呼ばれる領域があり、ランタイムコンスタントプールに格納されているのが実際です。コンパイラの不滅性。さまざまなリテラルとシンボル参照ですが、実行時に他の定数を追加できます。最も代表的なものは String の内部メソッドです。
5) cp_info は、上記のさまざまなリテラルとシンボル参照を含む定数プールを表します。 Java 仮想マシン仕様 Java SE 7 Edition の定数プールには合計 14 個のデータ項目があり、各定数は共通の部分タグを使用して型定数を示します。
具体的な詳細については以下で簡単に説明し、後の例で詳細を説明します。
CONSTANT_Utf8_info タグ フラグは 1、UTF-8 エンコード文字列 CONSTANT_Integer_info タグ フラグは 3、整数リテラル CONSTANT_Float_info タグ フラグは 4、浮動小数点リテラル CONSTANT_Long_info タグ フラグは 5、長整数リテラル CONSTANT_Double_info タグ フラグ ビットは 6、倍精度リテラル CONSTANT_Class_info タグのフラグは 7、クラスまたはインターフェイスのシンボリック参照 CONSTANT_String_info タグは 8、文字列型のリテラル CONSTANT_Fieldref_info タグは 9、フィールド CONSTANT_Methodref_info タグのシンボリック参照は 10、クラス内のメソッドのシンボリック参照 CONSTANT_InterfaceMethodref_info タグは 11、シンボリックインターフェース CONSTANT_NameAndType_info タグ内のメソッドへの参照フラグ ビット 12、フィールドとメソッドの名前、および型へのシンボリック参照
6) u2 access_flags は、次の図に示すように、クラスまたはインターフェイスのアクセス情報を表します。
7) u2 this_class はクラスの定数プール インデックスを表し、定数プール内の CONSTANT_Class_info の定数を指します。
8) u2 super_class はスーパークラスのインデックスを表し、定数プール内の CONSTANT_Class_info の定数を指します。
9) u2 Interface_counts はインターフェイスの数を表します。
10) u2 Interface[interface_counts] はインターフェイス テーブルを表し、その中の各項目は定数プール内の CONSTANT_Class_info 定数を指します。
11) u2 field_count は、クラスのインスタンス変数とクラス変数の数を表します。
12) field_info フィールド[fields_count] はフィールド テーブルの情報を表します。フィールド テーブルの構造は次のとおりです。
上の図では、access_flags はフィールドのアクセス表現を表します。たとえば、フィールドは public、private、protect です。など。name_index はフィールド名を表し、定数プール内の CONSTANT_UTF8_info 型の定数を指します。descriptor_index はフィールドの記述子を表し、定数プール内の CONSTANT_UTF8_info 型の定数も指します。attributes_count は属性テーブルの数を表します。フィールド テーブルおよび属性テーブル これは、フィールド、メソッド、およびクラス属性を記述するために使用される拡張可能な構造です。Java 仮想マシンのバージョンが異なれば、サポートされる属性テーブルの数も異なります。
13) u2methods_count はメソッドテーブルの数を表します。
14) Method_info はメソッド テーブルを表します。メソッド テーブルの具体的な構造は次の図に示すとおりです。
このうち、access_flags はメソッドのアクセス表現を表し、name_index は名前のインデックスを表し、descriptor_index はメソッドの記述子を表します。attributes_count とattribute_info はフィールド テーブルの属性テーブルと同様ですが、属性テーブルの属性が異なる点が異なります。フィールド テーブルとメソッド テーブルの は異なります。メソッド テーブルの Code 属性はメソッドのコードを表しますが、フィールド テーブルには Code 属性がありません。特定のクラスに属性がいくつあるかについては、後でクラス ファイル構造の属性テーブルを確認するときに説明します。
15)attribute_count は属性テーブルの数を表します。属性テーブルに関しては、次の点を明確にする必要があります。
属性テーブルは、クラス ファイル構造の最後、フィールド テーブル、メソッド テーブル、およびコード属性に存在します。つまり、属性テーブルは属性テーブル内に存在することもできます。属性テーブルの長さは固定されていません。属性が異なれば長さも異なります。
上記のクラスファイル構造における各項目の構成を説明した後、具体的な例を用いて以下の内容を説明します。
次のようにコードをコピーします。
パッケージ com.ejushang.TestClass;
パブリック クラス TestClass は Super{ を実装します
プライベート静的最終 int staticVar = 0;
プライベート int インスタンス変数 = 0;
public int instanceMethod(int param){
パラメータ+1を返します;
}
}
インターフェース スーパー{ }
jdk1.6.0_37 の javac を介してコンパイルされた TestClass.java に対応する TestClass.class のバイナリ構造を次の図に示します。
次に、先ほどのClassのファイル構造に基づいて、上図のバイトストリームを解析していきます。
1) マジックナンバー<br/>クラスのファイル構造から、上の図では、アドレス 00000000h ~ 00000003h の内容がマジックナンバーであることがわかります。 Class ファイルのマジックナンバーは 0xCAFEBABE です。
2) メジャー バージョン番号とマイナー バージョン番号<br/>次の 4 バイトはメジャー バージョン番号とマイナー バージョン番号です。上図から、00000004h から 00000005h までの対応する番号は 0x0000 であることがわかります。つまり、Class のminor_version です。は 0x0000、00000006h ~ 00000007h の対応するコンテンツは 0x0032 であるため、クラス ファイルの Major_version バージョンは 0x0032 です。これは、jdk1.6.0 によってコンパイルされたクラスに対応するメジャー バージョンとマイナー バージョンです。ターゲットパラメータ。
3) 定数プールの数<br/>次の 2 バイトは、00000008h ~ 00000009h の定数プールの数を表します。上図から、その値は 0x0018、つまり 10 進数で 24 であることがわかります。定数プールの数を明確にする必要があります。定数プールの数は constant_pool_count-1 です。インデックス 0 は、クラス内のデータ項目が定数プール内の定数を参照しないことを意味するためです。
4) 定数プール<br/>定数プールにはさまざまなタイプの定数があると述べました。TestClass.class の最初の定数を見てみましょう。各定数は u1 タイプのタグ識別子で表されます。定数のタイプ (上の図の 0000000ah)内容は 0x0A で、セカンダリ システムに変換すると 10 になります。上記の定数型の説明から、タグ 10 の定数は Constant_Methodref_info であることがわかり、Constant_Methodref_info の構造は次の図に示すとおりです。
このうち、class_index は定数プール内の CONSTANT_Class_info 型の定数を指します。TestClass のバイナリ ファイル構造から、class_index の値は 0x0004 (アドレスは 0000000bh-0000000ch) であることがわかります。 4番目の定数。
name_and_type_index は、定数プール内の CONSTANT_NameAndType_info 型の定数を指します。上の図からわかるように、name_and_type_index の値は 0x0013 で、定数プール内の 19 番目の定数を指していることを意味します。
次に、同じ方法を使用して、定数プール内のすべての定数を検索できます。ただし、JDK には、定数プールに含まれる定数を表示できる便利なツールが用意されています。定数プール内のすべての定数は、javap -verbose TestClass を通じて取得できます。スクリーンショットは次のとおりです。
上の図から、TestClass の定数プールに 24 個の定数があることが明確にわかります。0 番目の定数は、Class 内のデータ項目がクラス内の定数を参照しないことを示すために使用されるため、0 番目の定数を忘れないでください。一定のプール。上記の分析から、TestClass の最初の定数表現方法は、class_index が指す 4 番目の定数が java/lang/Object であり、name_and_type_index が指す 19 番目の定数値が <init>:()V であることがわかります。ここで、メソッドを表す最初の定数が、Java コンパイラによって生成されたインスタンス コンストラクター メソッドを表すことがわかります。定数プール内の他の定数も同様に分析できます。 OK、定数プールを分析したら、次に access_flags を分析しましょう。
5) u2 access_flags は、クラスまたはインターフェイスに関するアクセス情報を表します。たとえば、Class は、クラスであるかインターフェイスであるか、public、static、final などであるかどうかを表します。特定のアクセス フラグの意味については前述しました。TestClass のアクセス フラグを見てみましょう。 Class のアクセスフラグは 0000010dh ~ 0000010e で、その値は 0x0021 です。前述の各種アクセスフラグのフラグビットによれば、0x0021=0x0001|0x0020、つまり、 ACC_PUBLIC と ACC_SUPER は True で、ACC_PUBLIC は理解しやすく、ACC_SUPER は jdk1.2 以降にコンパイルされたクラスによって保持されるフラグです。
6) u2 this_class はクラスのインデックス値を表し、クラスの完全修飾名を表すために使用されます。クラスのインデックス値は次の図に示されています。
上の図から明らかなように、クラス インデックスの値は 0x0003 で、定数プールの 3 番目の定数に対応します。javap の結果から、3 番目の定数が CONSTANT_Class_info 型の定数であることがわかります。これでクラスの完全な詳細を知ることができます。修飾名は com/ejushang/TestClass /TestClass です。
7) u2 super_class は、現在のクラスの親クラスのインデックス値を表します。このインデックス値は、定数プール内の CONSTANT_Class_info 型の定数を指します。その値は次の図に示されています。 0x0004。定数プールの最初の 4 つの定数を確認すると、TestClass の親クラスの完全修飾名が java/lang/Object であることがわかります。
8)interfaces_countとinterfaces[interfaces_count]は、インターフェイスの数と各特定のインターフェイスを表します。TestClassのインターフェイスの数とインターフェイスは次の図に示すとおりです。0x0001はインターフェイスの数が1であることを意味し、0xはインターフェイスの数を意味します。 0005 は定数プール値内のインターフェイスのインデックスを意味します。定数プール内の 5 番目の定数を見つけます。そのタイプは CONSTANT_Class_info で、その値は com/ejushang/TestClass/Super です。
9) field_count と field_info 。field_count はクラス内の field_info テーブルの数を表し、field_info はクラスのインスタンス変数とクラス変数を表します。ここで、field_info には親クラスから継承されたフィールドは含まれないことに注意してください。 field_info は次の図に示すとおりです。
このうち、access_flags は、public、private、protected、static、final などのフィールドのアクセスフラグを表します。access_flags の値は、次の図に示すとおりです。
このうち、name_index と descriptor_index はどちらも定数プールのインデックス値で、それぞれフィールドの名前とフィールドの記述子を表します。フィールドの名前はわかりやすいですが、その記述子はどのように理解すればよいでしょうか。分野?実際、JVM 仕様では、フィールド記述子は次の図のように指定されています。
このうち、上の図の最後の行に注目してください。これは 1 次元配列の記述子を表しており、String[][] の記述子は [[ Ljava/lang/String になります。 int[][] 記号は [[I.次のattributes_countとattributes_infoは、それぞれ属性テーブルと属性テーブルの数を表します。上記の TestClass を例として、TestClass のフィールド テーブルを見てみましょう。
まず、TestClass のフィールド数を見てみましょう。
上の図からわかるように、TestClass には 2 つのフィールドがあります。TestClass のソース コードを見ると、実際には 2 つのフィールドしかないことがわかります。次に、最初のフィールドを見てみましょう。 private int staticVar、Class ファイル内のバイナリ表現は次のとおりです。
このうち、0x001A はアクセスフラグを表し、access_flags テーブルを見ると、ACC_PRIVATE、ACC_STATIC、ACC_FINAL であることがわかります。次に、0x0006 と 0x0007 は、それぞれ定数プールの 6 番目と 7 番目の定数を表します。定数プールを見ると、その値が staticVar と I であることがわかります。staticVar はフィールド名、I はフィールド記述子です。上記のディスクリプタの説明を通じて、私が記述したのは int 型の変数です。次に、0x0001 は staticVar フィールド テーブル内の属性テーブルの数を表します。上図から、対応する属性テーブルが 1 つあることがわかります。 0x0008 は定数プールの 8 番目の定数を表します。定数プールを見ると、この属性が ConstantValue 属性であることがわかります。ConstantValue 属性の形式は次のとおりです。
このうち、attribute_name_indexは属性名の定数プールインデックスを表し、この例ではConstantValueのattribute_lengthは2の固定長であり、constantValue_indexは定数プール内の参照を表す。 ×0009 9 番目の定数が表示されます。これは、値が 0 の CONSTANT_Integer_info 型の定数を表します。
private static Final int staticVar=0 について説明します。この例では、instanceVar のバイナリ表現は次の図のようになります。
このうち、0x0002はアクセスマークがACC_PRIVATEであることを意味し、0x000Aはフィールド名を意味し、定数プールを見るとフィールド名がinstanceVarであることがわかります。 0× 0007 は、定数プール内の 7 番目の定数を指すフィールドの記述子を表します。定数プールを見ると、7 番目の定数が、instanceVar のタイプを表すことがわかります。属性テーブルの数は 0 です。
10) Methods_count および Methods_info 。ここで、methods_count はメソッドの数を表し、methods_info はメソッド テーブルを表します。メソッド テーブルの構造は次の図に示すとおりです。
上の図からわかるように、method_info と field_info の構造は、メソッド テーブル内のすべてのフラグ ビットと access_flag の値が次の図に示すように非常に似ています。
このうち、name_index と descriptor_index はメソッドの名前と記述子を表し、それぞれ定数プールを指すインデックスです。ここでメソッド記述子について説明する必要があります。メソッド記述子の構造は次のとおりです。 (パラメータ リスト) 戻り値。たとえば、public int instanceMethod(int param) の記述子は次のようになります。これは、int を持つことを意味します。戻り値も int 型のメソッドです。次に属性の数と属性テーブルがありますが、メソッド テーブルとフィールド テーブルには両方とも属性の数と属性テーブルがあります。違う。次に、TestClass を使用したメソッド テーブルのバイナリ表現を見てみましょう。まず、メソッド テーブルの数を見てみましょう。スクリーンショットは次のとおりです。
上の図からわかるように、メソッド テーブルの数は 0x0002 です。これは、2 つのメソッドがあることを意味します。次に、最初のメソッドを分析しましょう。まず、TestClass の最初のメソッドの access_flag、name_index、descriptor_index を見てみましょう。スクリーンショットは次のとおりです。
上の図から、access_flags が 0x0001 であることがわかります。access_flags フラグの上記の説明から、メソッドの access_flags の値が ACC_PUBLIC であり、name_index が 0x000B であることがわかります。 11 番目の定数は、メソッドの名前が <init> であることがわかり、0x000C は descriptor_index を意味し、定数プール内の 12 番目の定数を意味し、その値は ()V です。これは、<init> メソッドにパラメーターと戻り値がないことを意味します。実際、これはコンパイラが自動的に生成したインスタンス コンストラクター メソッドです。次の 0x0001 は、<init> メソッドのメソッド テーブルに 1 つの属性があることを示します。属性のスクリーンショットは次のとおりです。
上の図からわかるように、0x000D に対応する定数プール内の定数はメソッドの Code 属性を表す Code です。したがって、ここではメソッドのコードが属性内の Code 属性に格納されることを理解してください。クラスファイルのメソッドテーブル内のテーブル。次に、Code 属性を分析します。Code 属性の構造を次の図に示します。
このうち、attribute_name_indexは定数プール内のCodeを値とする定数を指し、attribute_lengthの長さはCode属性テーブルの長さを示します(ただし、この長さにはattribute_name_indexとattribute_lengthの6バイトの長さは含まれません) )。
max_stack は最大スタック深度を表し、仮想マシンは実行時にこの値に基づいてスタック フレーム内のオペランドの深さを割り当てます。max_locals はローカル変数テーブルの記憶領域を表します。
max_locals の単位はスロットです。これは、実行時に仮想マシンがローカル変数にメモリを割り当てるための最小単位です。byte、char、int などの 32 ビット型を超えないデータ型は 1 を占めます。スロット、ダブルおよびロングさらに、ローカル変数がそのスコープを超えると、ローカル変数が再利用される可能性があるため、max_locals の値はすべてのローカル変数で必要なメモリの合計ではありません。占有されているスロットは再利用されます。
code_length はバイトコード命令の数を表し、code はバイトコード命令を表します。 上の図から、u1 タイプの値は 0x00-0xFF であり、対応する 10 進数は 0- であることがわかります。 255. 現在、仮想マシンの仕様では 200 を超える命令が定義されています。
Exception_table_length、Exception_tableは、それぞれメソッドに対応する例外情報を表す。
ここから、属性テーブルはクラス ファイル、メソッド テーブル、フィールドに存在することができ、非常に柔軟であることがわかります。テーブルとコード属性。
次に、上記の例の分析を続けます。上記の init メソッドの Code 属性のスクリーンショットから、属性テーブルの長さは 0x00000026、max_stack の値は 0x0002 であることがわかります。 max_locals の値は 0× 0001、code_length の長さは 0x0000000A、その後 00000149h- 00000152h はバイトコードです。次に、Exception_table_length の長さは 0x0000、attribute_count の値は 0x0001 で、定数プール内の属性の名前を表します。 14 番目の定数を取得するには、LineNumberTable、LineNu を使用します。 mberTable は、Java ソース コードの行番号とバイトコードの行番号の間の対応を記述するために使用されます。これは、実行時に -g:none コンパイラ パラメーターを使用してこの情報の生成をキャンセルすると、最大の影響が生じます。例外が発生した場合、スタック上にエラー行番号を表示できず、デバッグ時にソース コードに応じてブレークポイントを設定できません。 次に、以下に示す LineNumberTable の構造を見てみましょう。
このうち、attribute_name_indexは前述したように定数プールのインデックスを表し、attribute_lengthは属性の長さを表し、start_pcテーブルとline_numberテーブルはバイトコードの行番号とソースコードの行番号を表す。この例の LineNumberTable プロパティのバイト ストリームは次のとおりです。
上記の TestClass の最初のメソッドを分析した後、同じ方法で TestClass の 2 番目のメソッドを分析できます。スクリーンショットは次のとおりです。
このうち、access_flags は 0x0001、name_index は 0x000F、descriptor_index は 0x0010 となっており、定数プールを見ると、このメソッドが public int instanceMethod (int param) メソッドであることがわかります。上記と同様の方法で、instanceMethod の Code 属性が次の図のようになっていることを確認できます。
最後に、Class ファイルの属性を分析してみましょう。00000191h ~ 00000199h は Class ファイル内の属性テーブルで、0x0011 は属性の名前を表します。定数プールを見ると、属性名が SourceFile であることがわかります。図に示すように、SourceFile の構造を見てみましょう。
このうち、attribute_length は属性の長さであり、sourcefile_index は、値がソース コード ファイルの名前である定数プール内の定数を指します。この例では、SourceFile 属性のスクリーンショットは次のとおりです。
このうち、attribute_length は 0×00000002 で、長さは 2 バイト、sourcefile_index の値は 0×0012 となっているので、定数プールの 18 番目の定数を見ると、ソース コード ファイルの名前が TestClass であることがわかります。 .java
最後に、テクノロジーに興味のある友人同士でもっとコミュニケーションがとれることを願っています。個人の Weibo: (http://weibo.com/xmuzyq)