本連載はスクラッチで軽量Javaフレームワークの設計、実現方法を解説します。Javaの知識を深めながら、Spring FrameworkのようなAOPxDIフレームワークをゼロから作成してみます。
自力でAOPとDI機能を実現するため、ある程度のJVM知識を習得する必要です。ですから、本題の前にJVM知識を紹介していきたいです。クラスレイアウト定義の解説を始め、JVMランタイム仕組みを紹介し、ASMフレームワークでJava classを操作する方法からAOPとDI機能の実装を展開します。
では、早速クラスレイアウト仕様を解説します。以下の内容はJava仮想マシン仕様SE 7版を参照します。一部用語を英語のまま使用します。
テスト環境
- OS: Mac OSX 10.8.5
- Java: Oracle 1.7.0_40(64bit)
Java class format
バイナリJavaクラスファイルは以下の特徴があります。
- ファイルは8ビット(1バイト)のストリームで構成されます。8ビット以上のデータはBig-Endianの順番で保存します。いわば、高いバイトは低いアドレスに保存されます。(IBMのPowerPCプロセッサはこの順番を採用します。Intelのx86プロセッサは逆順番のLittle-Endianを採用します)。
- クラスのレイアウトはC言語の構造体のような可変長配列で構成されます。主に2つのデータ・タイプ(符号なし整数とテーブル)があります。
u1
: 符号なし8ビット整数u2
: Big-Endianバイト順の符号なし16ビット整数u4
: Big-Endianバイト順の符号なし32ビット整数テーブル
: いくつかの型の可変長の配列。テーブルのテーブル内の項目数はカウント数により識別されるが、テーブルのバイト内のサイズは項目それぞれを調査することのみで決定される。
Java仮想マシン仕様に記載されたJavaクラスの構造は以下のようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
コメントを見ればわかりますが、少し説明します。constant_pool[constant_pool_count-1]を一見すると配列ですが、各要素のタイプと長さは異なっています。
- u4 magic: マジックナンバーです。このファイルはpng画像ファイルではなく、JavaソースをコンパイルしたJava classファイルであることを示します。4バイトの0xCAFEBABEで固定です。
- u2 major_version: 使用されるクラスファイルフォーマットのメジャーバージョン数です。
- J2SE 7 = 51(0x33 十六進)
- J2SE 6.0 = 50(0x32 十六進)
- J2SE 5.0 = 49(0x31 十六進)
- JDK 1.4 = 48(0x30 十六進)
- u2 constant_pool_count: 定数プールのカウントです。
cp_info constant_pool[constant_pool_count-1]: 定数プールテーブル、リテラル数、文字列、そしてクラスやメソッドへの参照といった項目を含む、可変長の定数プールエントリです。 合計エントリ(定数テーブルカウント – 1)数を含む、1から始まり索引付けされます。Java SE 7 Editionに14種類のcp_infoはあります。tagの値で区別します。
種類 tag 内容 CONSTANT_Utf8_info 1 UTF-8 (Unicode) 文字列 CONSTANT_Integer_info 3 Integer : Big-Endianフォーマットによる符号付き32ビット2の補数 CONSTANT_Float_info 4 Float : 32ビット単精度IEEE 754浮動小数点数 CONSTANT_Long_info 5 Long : Big-Endianフォーマットによる符号付き64ビット2の補数(定数テーブルの2つのスロットを占める) CONSTANT_Double_info 6 Double : 64ビット倍精度IEEE 754浮動小数点数(定数テーブルの2つのスロットを占める) CONSTANT_Class_info 7 クラス参照 : (内部フォーマットによる)完全修飾型クラス名を含むUTF-8文字列による定数テーブル内のインデックス(Big-Endian) CONSTANT_String_info 8 文字列参照 : UTF-8による定数プール内のインデックス(Big-Endian) CONSTANT_Fieldref_info 9 フィールド参照 : 定数プール内にある2つのインデックス、最初はクラス参照で次は名前および型の記述(Big-Endian) CONSTANT_Methodref_info 10 メソッド参照 : 定数プール内にある2つのインデックス、最初はクラス参照で次は名前および型の記述(Big-Endian) CONSTANT_InterfaceMethodref_info 11 インタフェース参照 : 定数プール内にある2つのインデックス、最初はクラス参照で次は名前および型の記述(Big-Endian) CONSTANT_NameAndType_info 12 名前および型の記述 : UTF-8による定数プール内のインデックス、最初は名前(識別子)を表し次は特別にエンコードされた型 CONSTANT_MethodHandle_info 15 Java SE 7からinvokedynamicの対応 CONSTANT_MethodType_info 16 Java SE 7からinvokedynamicの対応 CONSTANT_InvokeDynamic_info 17 Java SE 7からinvokedynamicの対応 各cp_infoの詳細は後ほど使われる際に説明します。
u2 access_flags: ビットマスクによるアクセスフラグです。
フラグ 値 キーワード ACC_PUBLIC 0x0001 public ACC_FINAL 0x0010 final ACC_SUPER 0x0020 super ACC_INTERFACE 0x0200 interface ACC_ABSTRACT 0x0400 abstract ACC_SYNTHETIC 0x1000 synthetic ACC_ANNOTATION 0x2000 annotation ACC_ENUM 0x4000 enum u2 this_class: 定数プールにthisクラスの参照(CONSTANT_Class_info)
1 2 3 4 |
|
- u2 super_class: 親クラスの参照(CONSTANT_Class_info)
- u2 interface_counts: 実現したインタフェース数
- u2 interface[interface_counts]: インタフェース参照(CONSTANT_Class_info)
- u2 fields_count:クラス変数とインスタンス変数の個数
- field_info fields[fields_count]:フィールド参照(field_info)
1 2 3 4 5 6 7 |
|
アクセスフラグはここを参照できます。
- u2 methods_count: メソッド数
- method_info methods[methods_count]: メソッド参照(method_info)
1 2 3 4 5 6 7 |
|
アクセスフラグはここを参照できます。
- u2 attributes_count: 任意の属性数
attribute_info attributes[attributes_count]: 任意の属性参照
attribute_infoはクラスファイルの最後に置かれます。例外、ソース行番号、デバッグ情報、annotationなどの標準属性以外、ユーザ定義の属性もあります。
そして属性の長さは固定ではありません。属性にネスト属性を含むことも可能です。
サンプル
話はやや複雑になりますが、簡単な例をあげます。 以下の簡単なクラスを作成します。シンプルなクラスなので、annotation, 例外処理などがありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
jdk_1.7.0_40でコンパイルしたクラスは以下のようです。
Java仮想マシン仕様を参照しながら、上記のバイトコードを解読してみましょう。
- マジック・ナンバー
クラスファイルの先頭4バイトは、Javaのクラスファイルであることを示すマジックナンバーで、0xCAFEBABE固定です。
0000: CA FE BA BE
00 00 00 33 00 21 0A 00 06 00 1B 09 …….3.!……
- バージョン番号
次の4バイトは、クラスファイルが実行対象とするJavaバージョンを識別するバージョン番号です。前半2バイトがマイナー・バージョンで後半2バイトがメジャーバージョンとなります。
以下は、マイナーバージョンが0(0x0000)、メジャーバージョンが51(0x0033)を表します。上のテーブルによって、Java SE 7のバージョン番号は51ですね。
0000: CA FE BA BE 00 00 00 33
00 21 0A 00 06 00 1B 09 …….3.!……
- 定数プール数
リテラル、実行時に解決するメソッド、フィールド参照、などの各種定数を持つ定数プールの個数です。定数プールは1から数えますので、Sampleクラスは32(0x21 – 1)個の定数があります。
0000: CA FE BA BE 00 00 00 33 00 21
0A 00 06 00 1B 09 …….3.!……
- 定数プールの情報配列
定数プールのフォーマットは種類により異なります。種類は先頭1バイトのタグで決まります。
定数プールの詳細をみってみましょう。まず1番目の定数をみます。
0000: CA FE BA BE 00 00 00 33 00 21 0A
00 06 00 1B 09 …….3.!……
0x0aは10ですので、上のテーブルによってConstant_Methodref_infoの定数です。Constant_Methodref_infoの構造は以下のようです。
1 2 3 4 5 |
|
class_indexは0x06です。定数プールの6番目のCONSTANT_Class_info定数を指します。
0000: CA FE BA BE 00 00 00 33 00 21 0A 00 06
00 1B 09 …….3.!……
name_and_type_indexは0x1bです。定数プールの27番目のCONSTANT_NameAndType_info定数を参照します。
0000: CA FE BA BE 00 00 00 33 00 21 0A 00 06 00 1B
09 …….3.!……
定数プールの27番のデータは以下のようです。
0140: 6C 65 2E 6A 61 76 61 0C 00 0D 00 0E
0C 00 0B 00 le.java………
CONSTANT_NameAndType_infoの構造は以下のようです。
1 2 3 4 5 |
|
上記の方法を従って、すべての定数を解読できます。バイナリは読みづらいですが、JDKに便利なのツールを提供しています。
javap -version Sample.classでバイナリデータをJVMのアセンブリコードに変換します。定数プールの内容もリストアプされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
定数プールに32個定数があることをわかりますね。#1から数えますが、#0はクラスが定数プールを参照しないことを示します。 ですから、constant_pool_countの値は33(0x21)です。
上の1番目の定数のclass_indexは#6ですが、java.lang.Objectのメソッドをわかります。
完全修飾されたJavaのクラス名は、「java.lang.Object」のように慣例的にドットで区分けされますが、Java仮想マシン内部形式は「java/lang/Object」のように、代わりにスラッシュを使用します。
name_and_type_indexは#27のCONSTANT_NameAndType_info(上記のコードによると<init>メソッド)です。コンパイルの時に自動生成したデフォールトコンストラクターですね。
メソッドの引数と戻り値は()Vを指します。Java仮想マシン内部でデータタイプの表示を以下の表にまとめました。
BaseType Character | Type | Interpretation |
---|---|---|
B | byte | signed byte |
C | char | Unicode character |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
LClassname; | reference | an instance of class Classname |
S | short | signed short |
Z | boolean | true or flase |
[ | reference | one array dimension(一次元配列) |
V | void | return void |
上の表によって、()Vは引数無し、戻り値無しの意味です。
複雑な例をあげます。二次元配列String[][]は[[Ljava/lang/Stringを表します。int[][]なら[[Iを表します。
- アクセスフラグ
クラス宣言またはインタフェース宣言で使用する修飾子のビットマスクを表します。
01A0: 2F 4F 62 6A 65 63 74 00 21
00 05 00 06 00 00 00 /Object.!…….
Sampleクラスのアクセスフラグは0x0021 = 0x0001|0x0020(すなわち、ACC_PUBLIC|ACC_SUPER)です。
ACC_PUBLICはpublicですが、ACC_SUPERはJDK 1.2以降強制的に追加された修飾子です。
- this_class
Sampleクラス情報のインデックスです。
01A0: 2F 4F 62 6A 65 63 74 00 21 00 05
00 06 00 00 00 /Object.!…….
定数プールの#5はCONSTANT_Class_info定数です。クラス名name_indexは#31のCONSTANT_Utf8_info定数を参照します。
それによって、クラスの名がnet/codemelon/brisk/demo/jvm/Sampleであることはわかります。
- 親クラス
01A0: 2F 4F 62 6A 65 63 74 00 21 00 05 00 06
00 00 00 /Object.!…….
定数#6はjava/lang/Objectです。宣言していない場合、暗黙でjava.lang.Objectを継承しますね。
- インタフェース
インタフェース数とインタフェース情報配列はクラスを実現したインタフェース情報です。Sampleクラスはinterfaceがありませんので、interfaces_countは0x00です。
01A0: 2F 4F 62 6A 65 63 74 00 21 00 05 00 06 00 00
00 /Object.!…….
- インスタンス変数とクラス変数
Sampleクラスは2つのインスタンス変数と1つのクラス変数(合わせて3つ)があります。親クラスの変数を含まないことを注意してください。
変数の構造は上のfield_infoです。
01A0: 2F 4F 62 6A 65 63 74 00 21 00 05 00 06 00 00 00
/Object.!…….
01B0: 03
00 02 00 07 00 08 00 00 00 19 00 09 00 08 00 …………….
以下は変数nameの内容を示します。
もっと複雑な例としてクラス変数AOP_CLASS_SUFFIXを解析しましょう。
1
|
|
AOP_CLASS_SUFFIXのaccess_flagsはACC_PUBLIC | ACC_STATIC | ACC_FINAL (0x0001 | 0x0008 | 0x0010) = 0x19です。
01B0: 03 00 02 00 07 00 08 00 00 00 19
00 09 00 08 00 …………….
name_indexは0x09です。上の定数一覧によって、#9 = Utf8 AOP_CLASS_SUFFIXです。
01B0: 03 00 02 00 07 00 08 00 00 00 19 00 09
00 08 00 …………….
descriptor_indexは0x08ですが、上の定数一覧によって、#8 = Utf8 Ljava/lang/String;(java.lang.Stringインスタンス)です。
01B0: 03 00 02 00 07 00 08 00 00 00 19 00 09 00 08
00 …………….
次のattributes_countは0x0001です。1つの属性はあります。
01B0: 03 00 02 00 07 00 08 00 00 00 19 00 09 00 08 00
…………….
01C0: 01
00 0A 00 00 00 02 00 04 00 04 00 0B 00 0C 00 …………….
次の2バイトは0x000aです。上の定数プールの#10によると”ConstantValue”の属性です。
ConstantValueの構造は以下のようです。
1 2 3 4 5 |
|
attribute_name_indexはConstantValueの定数プールのインデックです。attribute_lengthは固定で2です。
01C0: 01 00 0A 00 00 00 02
00 04 00 04 00 0B 00 0C 00 …………….
constantvalue_indexは0x04です。定数プールによると、#4 = String #30 // $$_brisk_aop_enhancedです。AOP_CLASS_SUFFIXの初期値ですね。
01C0: 01 00 0A 00 00 00 02 00 04
00 04 00 0B 00 0C 00 …………….
他の変数は同じな方法で解析できます。
- メソッド
コンパイラで自動生成したコンストラクターを含めて、methods_countは0x04です。
01D0: 00 00 04
00 01 00 0D 00 0E 00 01 00 0F 00 00 00 …………….
一番目のmethod_infoの内容を見てみましょう。method_infoの構造を上に参照できます。
access_flagsは0x01です。
01D0: 00 00 04 00 01
00 0D 00 0E 00 01 00 0F 00 00 00 …………….
メソッドのアクセスフラグ一覧によると、ACC_PUBLICは0x0001です。
01D0: 00 00 04 00 01 00 0D
00 0E 00 01 00 0F 00 00 00 …………….
name_indexは0x0dです。上の定数プールによると、#13 = Utf8 <init>です。自動生成したデフォルト・コンストラクターです。
01D0: 00 00 04 00 01 00 0D 00 0E
00 01 00 0F 00 00 00 …………….
descriptor_indexは0x0eなので、定数プールの#14 = Utf8 ()Vです。デフォルト・コンストラクターはパラメータ無し、戻り値voidです。
次のattributes_countは0x01です。
01D0: 00 00 04 00 01 00 0D 00 0E 00 01
00 0F 00 00 00 …………….
次の2バイトはattribute_name_indexです。定数プールの0x0fは#15 = Utf8 Codeです。
それによって、属性タイプはCode_attributeです。
01D0: 00 00 04 00 01 00 0D 00 0E 00 01 00 0F
00 00 00 …………….
Code_attributeの構造は以下のようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
かなり複雑な構造ですね。メソッドに仮想マシンの命令を表す構造体です。
01D0: 00 00 04 00 01 00 0D 00 0E 00 01 00 0F 00 00 00
…………….
01E0: 39
00 02 00 01 00 00 00 0B 2A B7 00 01 2A 10 1E 9……..…..
attribute_lengthはCode_attributeにattribute_name_indexとattribute_lengthを除いたバイト数です。
上のバイトデータによると、<init>のCode_attributeの長さは0x39バイトです。
01E0: 39 00 02 00 01
00 00 00 0B 2A B7 00 01 2A 10 1E 9……..…..
次のmax_stackは0x02です。max_localsは0x01です。JVMでメソッドをframeに実行されます。
frameにローカル変数用の配列と操作命令スタックはあります。変数配列はメソッドパラメータ、ローカル変数(中間結果)を保存します。
操作スタックは仮想マシンの命令と操作数(変数配列からロードされる)を順番でロードして実行します。結果を変数配列仁保存し、命令と操作数をクリアし、次の命令を処理します。
メソッドのすべてのコードを実行する時、変数配列の最大長さはmax_localsと呼ばれます。操作スタックの最大長さはmax_stackと呼ばれます。
doubleとlongのデータは64ビットなので、max_stackとmax_localsを計算するときに注意しなければなりません。
詳しいJVMのランタイム仕組みは次回に解説させて頂きます。
01E0: 39 00 02 00 01 00 00 00 0B
2A B7 00 01 2A 10 1E 9……..…..
code_lengthは0x0bです。メソッドコードはcode[11]に置かれます。Code_attributeの構成によって、codeタイプはu1です。
u1の範囲は0x00 ~ 0xff(0 ~ 255)です。現在約200個のJVM命令を定義しています。
exception_table_lengthとexception_tableは例外情報です。<init>は例外宣言がありませんので、exception_table_lengthは0です。
01F0: B5 00 02 B1 00 00
00 02 00 10 00 00 00 0A 00 02 …………….
次のattributes_countは0x02です。
01F0: B5 00 02 B1 00 00 00 02
00 10 00 00 00 0A 00 02 …………….
1つ目の属性のインデックは0x10です。定数プールの#16はUtf8 LineNumberTableです。
01F0: B5 00 02 B1 00 00 00 02 00 10
00 00 00 0A 00 02 …………….
LineNumberTableの構造は以下のようです。
1 2 3 4 5 6 7 8 |
|
attribute_lengthはattribute_name_indexとattribute_length以外のバイト数です。
バイトデータによって、attribute_lengthは10(0x0a)です。
01F0: B5 00 02 B1 00 00 00 02 00 10 00 00 00 0A
00 02 …………….
line_number_table_lengthは0x02です。
01F0: B5 00 02 B1 00 00 00 02 00 10 00 00 00 0A *00 02` …………….
line_number_tableは次の8バイトとです。start_pcはJVM命令の番号です。line_numberはソースの行番号です。
0200: 00 00 00 08 00 04 00 0E
00 11 00 00 00 0C 00 01 …………….
次の属性は定数プールの#17(0x11) = Utf8 LocalVariableTableです。
0200: 00 00 00 08 00 04 00 0E 00 11
00 00 00 0C 00 01 …………….
LocalVariableTableの構造は以下のようです。
1 2 3 4 5 6 7 8 9 10 11 |
|
attribute_lengthは0x0cです。local_variable_table_lengthは0x01です。local_variable_tableに1つの変数があることはわかります。
0200: 00 00 00 08 00 04 00 0E 00 11 00 00 00 0C 00 01
…………….
次の10バイトはlocal_variable_table[1]の変数です。
0210: 00 00 00 0B 00 12 00 13 00 00
00 01 00 14 00 15 …………….
定数プールとlocal_variable_tableの構造によって、上記のデータの意味は以下のようです。
1 2 3 4 5 6 7 |
|
start_pc + lengthはメソッド実行の開始JVMコマンドの位置を表します。name_indexとdescriptor_indexはSampleインスタンスthisのことが分かります。
indexはthis変数がローカル変数配列の最初(インデックス0)位置に置かれることを示します。JVMではすべてのメソッド実行frameの変数配列の0にthis変数を置かれます。
同様のように他のメソッドを解析できます。次のバイトをみってみましょう。
0210: 00 00 00 0B 00 12 00 13 00 00 00 01 00 14 00 15
…………….
0x0001はACC_PUBLICです。0x0014は定数プールの#20 = Utf8 initです。0x0015は定数プールの#21 = Utf8 (Ljava/lang/String;I)Vです。
public void init(String name, int age)メソッドであることがわかりますね。
最後はattributes_countとattribute_info attributes[attributes_count]を見ます。
02D0: 00 00 01 00 10 00 00 00 06 00 01 00 00 00 1A 00
…………….
02E0: 01 00 19 00 00 00 02 00 1A
………
attributes_countは0x0001です。属性タイプは定数プールの#25(0x0019) = Utf8 SourceFileです。 SourceFile属性の構造は以下のようです。
1 2 3 4 5 |
|
attribute_lengthは固定で2です。sourcefile_indexは定数プールの#26(0x001a) Utf8 Sample.javaです。 ソースファイルの名前はSample.javaであることはわかります。
まとめ
以上では簡単なクラスを例として、Javaクラスファイルのレイアウトを解析してみました。exception_table、annotationなどの構造を触れていません。
詳しい内容はJava仮想マシン仕様の第四章を参照すれば良いと思います。
次回はJVMランタイム仕組みを紹介していきたいです。JavaコードとJVMアセンブリコードを対照しながら、JVMの内部動作を考査してみます。