バイトコードレベル解析クラスのクラスファイル構造

このクラスでは、クラスファイルの構造をバイトコードレベルから解析します。まずは面接の質問を見てみましょう。

JavaのString文字列の長さに制限はありますか?

通常のプロジェクト開発では、String str = "abc" など、String を使用して文字列を宣言することがよくありますが、等号の後の文字列定数に長さの制限があるかどうかについて考えたことはないかもしれません。この質問に完全に答えるには、今日クラス ファイルについて説明したことを学ぶ必要があります。

クラスの内外

Java は「一度コンパイルすればどこでも実行できる」を実現でき、クラス ファイルがほとんどの功績を占めるはずです。Java 言語に優れたクロスプラットフォーム機能を持たせるために、Java はすべてのプラットフォームで使用できる中間コード (バイトコード クラス ファイル (.class ファイル))を独自に提供しています。バイトコードの場合、仮想マシンがインストールされていれば、プラットフォームの種類 (Mac、Windows、Linux など) に関係なく、バイトコードを直接実行できます。

また、バイトコードを使用すると、Java 仮想マシンと Java 言語の間の結合も切り離されますこの文をよく理解できないかもしれませんが、このデカップリングは何を指しているのでしょうか?

実際、Java 仮想マシンの目的は Java 言語を実行することだけではありません。現在、Java 仮想マシンは、Groovy、JRuby、Jython、Scala など、Java 言語以外の多くの言語をすでにサポートしています。他の言語をサポートできる理由は、これらの言語でも、コンパイル後に JVM によって解析および実行できるバイトコード ファイルを生成できるためです。仮想マシンは、バイトコードがどの言語でコンパイルされたかを気にしません。以下に示すように:

神の視点からクラスファイルを見てください

クラス ファイルを全体的な観点から見ると、クラス ファイルにはunsigned Numbertablesの 2 つのデータ構造しかありません。

  • 符号なし数値: 基本データ型に属します。U1、u2、u4、u8 はそれぞれ 1 バイト、2 バイト、4 バイト、8 バイトの符号なし数値を表します。符号なし数値は、数値、インデックス参照、数量値の記述に使用できます、または文字列 (UTF-8 エンコード)。

  • テーブル: テーブルは、複数の符号なし数値または他のテーブルをデータ項目として構成された複合データ型であり、クラス ファイル内のすべてのテーブルは "_info" で終わります実際、クラス ファイル全体は本質的にテーブルです。

2 つの関係は次の図で表すことができます。

他の符号なし数値や他のテーブルをテーブルに含めることができることがわかります。疑似コードは次のようになります。

// 无符号数

u1 = byte[1];

u2 = byte[2];

u4 = byte[4];

u8 = byte[8];



// 表

class_table {
    
    
    // 表中可以引用各种无符号数,
    u1 tag;
    u2 index2;
    ...
    // 表中也可以引用其它表
    method_table mt;
    ...
}

クラスファイルの構造

先ほど、クラス ファイルには符号なし数値とテーブルの 2 つのデータ構造しかないと言った。そして、これらの符号なしの数値とテーブルは、クラス内のさまざまな構造を構成します。これらの構造は、隣接するアイテムの間に隙間がなく、所定の順序で前後にぴったりと配置されています。以下に示すように:

JVM がクラス ファイルをロードするとき、JVM は上図の構造に従ってクラス ファイルを解析し、クラス ファイルをメモリにロードし、メモリに対応する領域を割り当てます。特定の構造は多くのスペースを占有する必要があります。次の図を参照してください。

これを見ると、概念的に少し混乱し、符号なしの数値、テーブル、および上記の構造間の関係が分からなくなるかもしれません。実際、簡単な例を挙げると、人体は H、O、C、N などの元素で構成されています。しかし、これらの要素は特定の規則に従って人体のさまざまな器官を形成します。クラスファイル内の符号なしの数字や表は人体のH、O、C、Nなどの要素に相当し、クラス構造図内の構造は人体のさまざまな器官に相当します。そして、これらの臓器の組織順序には厳格な順序要件があり、結局のところ、お尻には目は成長できません。

事例分析

これらの概念を明確にした後、Java コード例を通じて上記の構造の詳細を見てみましょう。まず、次のように単純な Java ソース コード Test.java を作成します。

import java.io.Serializable;

 

public class Test implements Serializable, Cloneable{
    
    

      private int num = 1;

 

      public int add(int i) {
    
    

          int j = 10;

          num = num + i;

          return num;

     }

}

javac を介してコンパイルして、Test.class バイトコード ファイルを生成します。次に、16 進エディタを使用してクラス ファイルを開くと、表示される内容は次のとおりです。

上の図は 16 進数で、2 文字ごとに 1 バイトを表します。一見、文字間に規則はありませんが、JVM の観点から見ると、これらの 16 進文字は厳密な規則に従って配置されています。次に、JVM がそれらをどのように解析するかを段階的に見てみましょう。

マジックナンバー マジックナンバー

上の図に示すように、クラス ファイルの先頭の 4 バイトはクラス ファイルのマジック ナンバーであり、固定値 - 0XCAFEBABE です。マジックナンバーはクラスファイルのマーク、つまりクラス形式ファイルかどうかを判断する基準で、最初の4バイトが0XCAFEBABEでなければクラスファイルではないことを意味し、 JVM によって認識またはロードできません。

バージョンナンバー

マジックナンバーに続く 4 バイトは、現在のクラス ファイルのバージョン番号を表します。最初の 2 バイト0000 はマイナー バージョン番号 (minor_version) を表し、最後の 2 バイト0034はメジャー バージョン番号 (major_version) で、対応する 10 進数値は 52、つまり現在のクラス ファイルのメジャー バージョン番号です。は 52、マイナー バージョン番号は 0 です。したがって、統合バージョン番号は 52.0、つまり  jdk1.8.0になります。

定数プール (強調)

バージョン番号のすぐ後には、定数プール (cp_info) と呼ばれるテーブルがあります。コンスタントプールには、クラス名、親クラス名、クラス内のメソッド名、パラメータ名、パラメータの型など、クラスに関連するさまざまな情報が保存されます。中間のさまざまなテーブルの形式で定数プールに保存されます。

定数プール内の各項目は、次の表に示すように、14 個の項目タイプを含むテーブルです。

定数プール内の各項目が u1 サイズのタグ値を持つことがわかります。タグ値はテーブルの識別子であり、JVM がクラス ファイルを解析するときに、この値を使用して現在のデータ構造がどのテーブルであるかを判断します。上記 14 種類のテーブルはそれぞれ独自の構造を持っているため、ここでは 1 つずつ紹介しませんが、他のテーブルも基本的に同様であるため、CONSTANT_Class_info と CONSTANT_Utf8_info の 2 つのテーブルを例として使用します。

まず、CONSTANT_Class_info テーブルの具体的な構造は次のとおりです。

table CONSTANT_Class_info {
    
    

    u1  tag = 7;

    u2  name_index;

}

説明する。

  • tag: バイト サイズを占めます。たとえば、値が 7 の場合、CONSTANT_Class_info タイプのテーブルであることを意味します。

  • name_index: これはインデックス値であり、定数プール内の name_index がインデックスである定数テーブルへのポインタとして理解できます。たとえば、name_index = 2 の場合、定数プール内の 2 番目の定数を指します。

次に、次のように CONSTANT_Utf8_info テーブルの具体的な構造を確認します。

table CONSTANT_utf8_info {
    
    

    u1  tag;

    u2  length;

    u1[] bytes;

}

説明する:

  • tag: 値は 1 で、CONSTANT_Utf8_info タイプのテーブルであることを意味します。

  • length: length は u1[] の長さを示します (例: length=5 は、次のデータが 5 つ連続する u1 タイプのデータであることを意味します)。

  • bytes: u1 型の配列、長さは上記の 2 番目のパラメーターの長さの値です。

クラス ファイルの Java コードで宣言した String 文字列の最終的な保存形式は、CONSTANT_utf8_info です。したがって、文字列の最大長は、u2 が表現できる最大値は 65536 ですが、NULL 値を保存するために 2 バイトを使用する必要があるため、文字列の最大長は 65536 - 2 = 65534 となります。「Java 文字列の最大長の分析」を参照してください 

定数プール内のテーブルにも相互参照があることを確認するのは難しくありません。次の図に示すように、図を使用して、CONSTANT_Class_info テーブルと CONSTANT_utf8_info テーブルの関係を理解し​​ます。

定数プール内のデータ構造を理解した後、サンプル コードの解析プロセスを見てみましょう。通常、開発者はさまざまな Java クラスを定義するため、クラス内のメソッドとパラメーターも異なります。したがって、定数プール内の要素の数は固定できないため、クラス ファイルは定数プールの前に 2 バイトの容量カウンターを使用して、現在のクラスの定数プールのサイズを表します。以下に示すように:

赤枠内の001dを10進数に変換すると29、つまり定数カウンタの値は29となります。このうち、添え字 0 の定数は、他の特別な目的のために JVM によって予約されているため、Test.class の実際の定数プール サイズは、このカウンターの値から 1 を引いた値、つまり 28 になります。

最初の定数は次のようになります。

0a を 10進数に変換すると 10 になります。定数プールの 14 個のテーブルを見ると、tag=10 のテーブル型が CONSTANT_Methodref_info であることがわかります。したがって、定数プールの最初の定数型がメソッド参照テーブルになります。 。その構造は次のとおりです。

CONSTANT_Methodref_info {
    
    

    u1 tag = 10;

    u2 class_index;        指向此方法的所属类

    u2 name_type_index;    指向此方法的名称和类型



}

つまり、「0a」の後の 2 バイトはこのメソッドが属するクラスを指し、次の 2 バイトはこのメソッドの名前と型を指します。それらの値は次のとおりです。

  • 0006: 10 進数 6、定数プール内の 6 番目の定数を指すことを意味します。

  • 0015: 10 進数の 21。定数プール内の 21 番目の定数を指すことを示します。

この時点で、最初の定数の解釈が完了します。直後に 2 番目の定数が続きます。

タグ09 はフィールド参照テーブル CONSTANT_FIeldref_info を意味し、その構造は次のとおりです。

CONSTANT_Fieldref_info{
    
    

    u1 tag;

    u2 class_index;        指向此字段的所属类

    u2 name_type_index;    指向此字段的名称和类型



}

こちらも4バイトで、インデックスが前後に2つあります。

  • 0005: 定数プール内の 5 番目の定数を指します。

  • 0016: 定数プール内の 22 番目の定数を指します。

これまでに、定数プール内の 2 つの定数を解決しました。残りの 21 個の定数の解析プロセスも同様であるため、ここでは 1 つずつ分析しません。実際、javap コマンドを使用すると、クラス定数プールの内容を表示できます。

javap -v Test.class

上記のコマンドを実行すると、表示される結果は次のようになります。

先ほど分析したように、定数プールの最初の定数は Methodref 型で、添字 6 と添字 21 の定数を指します。添字 21 の定数タイプは NameAndType で、対応するデータ構造は次のとおりです。

CONSTANT_NameAndType_info{
    
    

    u1 tag;

    u2 name_index;    指向某字段或方法的名称字符串

    u2 type_index;    指向某字段或方法的类型字符串
}

21 に添字が付いた NameAndType の name_index と type_index は、それぞれ 13 と 14、つまり "<init>" と "()V" を指します。したがって、解析プロセスと定数プール内の最初の定数の最終値は、次の図に示すように最終的に解析されます。

レイヤーごとの参照を注意深く分析すると、最終的に、Test.class ファイルの定数プールの最初の定数が、デフォルトのコンストラクター メソッドを Object に保存していることがわかります。

アクセスフラグ (access_flags)

次の図に示すように、定数プールの直後の定数はアクセス フラグであり、2 バイトを占めます。

アクセス フラグは、クラス ファイルがクラスであるかインターフェイスであるか、public として定義されているかどうか、abstract であるかどうか、クラスである場合はクラス ファイルがクラスとして宣言されているかどうかなど、クラスまたはインターフェイスのアクセス情報を表します。ファイナルなど さまざまなアクセス フラグは次のとおりです。

私たちが定義した Test.java は通常の Java クラスであり、インターフェイス、列挙、または注釈ではありません。そして、それは public によって変更されますが、final および abstract として宣言されていないため、対応する access_flags は0021 (0X0001 と 0X0020 の組み合わせ) です。

クラスインデックス、親クラスインデックス、およびインターフェイスインデックスカウンター

アクセス フラグの後の 2 バイトはクラス インデックス、クラス インデックスの後の 2 バイトは親クラス インデックス、親クラス インデックスの後の 2 バイトはインターフェイス インデックス カウンタです。以下に示すように:

クラス インデックスは定数プールの 5 番目の定数を指し、親クラス インデックスは定数プールの 6 番目の定数を指し、実装されているインターフェイスの数は 2 であることがわかります。定数プール内のデータをもう一度確認します。

この図から、5 番目と 6 番目の定数は両方とも CONSTANT_Class_info テーブル タイプであり、それらが表すクラスはそれぞれ「Test」と「Object」であることがわかります。インターフェイス カウンタをもう一度見てください。インターフェイス カウンタの値は 2 なので、このクラスが 2 つのインターフェイスを実装していることを意味します。インターフェイス カウンターの後の 4 バイトが次であることを確認します。

  • 0007: 定数プールの7番目の定数を指します。図から7番目の定数の値が「Serializable」であることがわかります。

  • 0008: 定数プールの 8 番目の定数を指します。図から 8 番目の定数の値が「Cloneable」であることがわかります。

要約すると、次の結論が得られます。現在のクラスは Test であり、Object クラスを継承し、2 つのインターフェイス「Serializable」と「Cloneable」を実装します。

フィールドテーブル

インターフェイス インデックスの直後にはフィールド テーブルが設定されており、フィールド テーブルの主な機能は、クラスまたはインターフェイスで宣言された変数を記述することです。ここでのフィールドにはクラスレベル変数とインスタンス変数が含まれますが、メソッド内で宣言されたローカル変数は含まれません。

同様に、クラス内の変数の数は固定されていないため、以下に示すように、フィールド テーブルのコレクションの前にカウンターを使用して変数の数を表します。

0002 は、 2 つの変数 (クラス ファイルではフィールドと呼ばれる) がクラスで宣言され、フィールド カウンターの後に 2 つのフィールド テーブルのデータ構造が続くことを意味します。

フィールド テーブルの具体的な構造は次のとおりです。

CONSTANT_Fieldref_info{
    
    

    u2  access_flags    字段的访问标志

    u2  name_index          字段的名称索引(也就是变量名)

    u2  descriptor_index    字段的描述索引(也就是变量的类型)

    u2  attributes_count    属性计数器

    attribute_info

}

Text.class のフィールド テーブルの解析を続けます。その構造は次の図に示されています。

フィールドアクセスフラグ

Java クラスの変数の場合、public、private、final、static などの識別子も識別に使用できます。したがって、フィールドを解析するときは、最初にそのアクセス フラグを判断する必要があります。フィールドのアクセス フラグは次のとおりです。

フィールドテーブル構造図のアクセスフラグの値は0002であり、プライベート型であることを意味します。変数名インデックスは定数プール内の 9 番目の定数を指し、変数名タ​​イプ インデックスは定数プール内の 10 番目の定数を指します。次のように、9 番目と 10 番目の定数はそれぞれ「num」と「I」です。

したがって、型が int のクラスに num という名前の変数があることがわかります。2番目の変数の分析処理についても同様ですので、あまり紹介しません。

予防:

  1. 親クラスまたは親インターフェイスから継承されたフィールドは、フィールド テーブル コレクションにはリストされません。

  2. 内部クラスで外部クラスへのアクセスを維持するために、外部クラスのインスタンスを指すフィールドが自動的に追加されます。

上記の 2 つのケースでは、クラスを定義して表示し、手動で分析することをお勧めします。

メソッドテーブル

フィールド テーブルの後にはメソッド テーブルの定数が続きます。図に示すように、クラス内のメソッドの数は固定されていないため、メソッド テーブル定数もカウンターで始まる必要があることも推測できると思います。

上の図は、Test.class に 2 つのメソッドがあることを示していますが、Test.java で宣言した add メソッドは 1 つだけです。これは、デフォルトのコンストラクター メソッドもメソッド テーブル定数に含まれているためです。

メソッドテーブルの構造は次のとおりです。

CONSTANT_Methodref_info{
    
    

    u2  access_flags;        方法的访问标志

    u2  name_index;          指向方法名的索引

    u2  descriptor_index;    指向方法类型的索引

    u2  attributes_count;    方法属性计数器

    attribute_info attributes;

}

ご覧のとおり、このメソッドには次のように独自のアクセス フラグもあります。

次のように、主に add メソッドを見てみましょう。

この図から、add メソッドの次のフィールドの具体的な値がわかります。

  1. access_flags  = 0001は 、アクセス許可がパブリックであることを意味します。

  2. name_index  = 0X 0011  は、定数プール内の 17 番目の定数、つまり「add」を指します。

  3. type_index  = 0X 0012   は、定数プール内の 18 番目の定数 (I) を指します。このメソッドは、int 型パラメータを受け取り、int 型パラメータを返します。

属性テーブル

以前にフィールドとメソッドを解析すると、その特定の構造内に、attributes_info というテーブル (属性テーブル) が表示されることがわかります。

属性テーブルは固定された構造を持たず、さまざまな属性が次の構造を満たすだけで済みます。

CONSTANT_Attribute_info{
    
    

    u2 name_index;

    u2 attribute_length length;

    u1[] info;

}

JVM には多くのプロパティ テーブルが事前定義されていますが、ここでは Code プロパティ テーブルに焦点を当てます。

  • コード属性テーブル

メソッド テーブルの分析を続けることができます。

ご覧のとおり、メソッド タイプ インデックスの後には、「add」メソッドの属性が続きます。0X0001は属性カウンターであり、属性が 1 つだけであることを意味します。0X000fは属性テーブル タイプのインデックスです。定数プールを見ると、以下に示すように、Code 属性テーブルであることがわかります。

コード属性テーブルで最も重要なのは、一部の列のバイトコードです。javap -v Test.class を渡すと、メソッドのバイトコードが表示されます。次の図は、add メソッドのバイトコード命令を示しています。

JVM が add メソッドを実行するとき、この一連の命令を使用して対応する操作を実行します。

要約:

このクラスでは、主にクラス ファイル コンテンツのデータ構造がどのようなものかを理解し、Test.class を使用して Java 仮想マシンがバイトコード ファイルを解析するプロセスをシミュレートおよびデモンストレーションします。このうち、クラス定数プール部分がキーとなる内容であり、クラスファイルにおけるリソースウェアハウスに相当し、他の構造も多かれ少なかれ最終的にはこのリソースウェアハウスを指すことになります。実際、通常は .class ファイルを直接開くために 16 進エディタを使用しません。javap などのコマンドやその他のツールを使用すると、クラス内のデータ構造を表示できます。ただ、自分で実行して JVM の解析プロセスを理解し、クラス ファイル構造の記憶を深めることは非常に役立ちます。

おすすめ

転載: blog.csdn.net/gqg_guan/article/details/132366496