S-JIS[2007-11-11] 変更履歴
Javaのソースをコンパイルするとclassファイルが作られるが、その中身はバイトコードと呼ばれる中間形式。
JavaVMがそのバイトコードを解釈して実行する。
classファイルの中身には色々な情報が入っているが、当ページではその中のプログラムに関する部分をメモしている。
バイトコード(インストラクションコード?)はいわばアセンブリ言語のようなもので、javapやjadを使うとclassファイルから簡単に逆アセンブルすることが出来る。
javapはJDKをインストールすれば使えるし、jadはJDK1.5以降には対応してないっぽいので、素直にjavapを使うのがいいんだろうなー。
C:\sample\classes> javap -c jp/hishidama/sample/Sample C:\sample\classes> javap -c jp.hishidama.sample.Sample C:\sample\classes\jp\hishidama\sample> javap -c Sample →コンソールに表示される
C:\sample\classes> jad -dis jp/hishidama/sample/Sample.class C:\sample\classes> jad -dis jp/hishidama/sample/Sample C:\sample\classes\jp\hishidama\sample> jad -dis Sample →カレントディレクトリにSample.jadが作られる(中身は逆アセンブルしたもの)
インストラクションコードはアセンブリ言語に近い(それくらい低水準言語だということ)。
レジスターというものが無く、一時的に使うデータは全てスタック上に置く。
スタックの単位(基本的なデータサイズ)はたぶん4バイト。longとdoubleだけ、2倍の領域
(1つのlongにつき2つのエリア)を占有するっぽい。
文字列定数やクラス名・メソッド名、さらには2バイトより大きい数値など、全ての定数は別領域に置かれ、番号(2バイト)が振られる。各命令からはその番号でアクセスする。つまり、一クラス内では定数は65536個しか使用できないってこと??まぁそんなデカいクラスなんぞ作るなっちゅう話だが(苦笑)
ほとんどの命令は、暗黙にスタックからデータをPOPしてPUSHする。すなわち、
戻り値の無い命令の場合、自分が必要とするデータをスタックの先頭から取得(POP)して演算する。
戻り値の有る命令の場合、自分が必要とするデータをスタックの先頭から取得(POP)し、結果をスタックに保存(PUSH)する。
命令によって必要なデータ数は異なるので、POPする個数も異なる。
POPすることによって、その分のデータはスタック上から消える。
スタック内の別の場所に、ローカル変数を保持する為の領域が確保される。
ローカル変数専用の命令を使ってアクセスするが、これも番号で管理される。
なお、0番はthis(実行中の自分のオブジェクト)を表しているらしい。
各命令(インストラクションコード)のニーモニックは英略語で表されている。
ほとんどの命令は 先頭1文字が型を表し、残りが動作内容を示唆している。例えばistoreのi、lstoreのl、astoreのa
i | integer | 数値(整数) |
l | long | 数値(整数) |
f | float | 数値(浮動小数) |
d | double | 数値(浮動小数) |
a | address | オブジェクト(参照型。当然配列も含む) |
変数に値を代入して加算する例。
int n = 123; int m = 3; int t = m + n;
↓
0: bipush 123 ←123をスタックに入れる(PUSH) 2: istore_1 ←スタックから値を取り出し(POP)、ローカル変数1に入れる 3: iconst_3 ←定数3をスタックに入れる(PUSH) 4: istore_2 ←スタックから値を取り出し(POP)、ローカル変数2に入れる 5: iload_2 ←ローカル変数2をスタックに入れる(PUSH) 6: iload_1 ←ローカル変数1をスタックに入れる(PUSH) 7: iadd ←スタックから値を2つ取り出し(POP×2)、加算して結果をスタックに入れる(PUSH) 8: istore_3 ←スタックから値を取り出し、ローカル変数3に入れる
開始 | → | → | → | → | → | → | → | → | ||
命令 | bipush 123 | istore_1 | iconst_3 | istore_2 | iload_2 | iload_1 | iadd | istore_3 | ||
命令実行後の スタック の状態 |
||||||||||
123 | ||||||||||
123 | 3 | 3 | 3 | 126 | ||||||
命令実行後の ローカル変数 の状態 |
3 | 126 | ||||||||
2 | 3 | 3 | 3 | 3 | 3 | |||||
1 | 123 | 123 | 123 | 123 | 123 | 123 | 123 | |||
0 | this | this | this | this | this | this | this | this | this |
スタックに整数を入れる命令はbipushだのiconstだのsipushだのがある。
0,1,2とかの小さい数値はよく使われるので、iconstという(効率の良い)専用の命令がある。
数値が1バイト以内ならbipush、2バイトだとsipush、それを超えると定数エリアに追いやられてldcが使われる。
「スタックにPUSHする」とは、スタック領域の空いている部分(最上位)に値を入れること。これを「スタックに積む」と呼ぶことがある。上の図を見ると
そう呼ぶ雰囲気が分かる、かな?
「スタックからPOPする」とは、スタックの最上位から値を取り出すこと。
オブジェクトを作ってメソッドを呼び出す例。
int n = print("メッセージ", new Object());
↓
0: aload_0 1: ldc #45; //String メッセージ 3: new #3; //class java/lang/Object 6: dup 7: invokespecial #8; //Method java/lang/Object."<init>":()V 10: invokevirtual #47; //Method print:(Ljava/lang/String;Ljava/lang/Object;)I 13: istore_1
開始 | → | → | → | → | → | → | → | ||
命令 | aload_0 | ldc #45 | new #3 | dup | invokespecial #8 | invokevirtual #47 | istore_1 | ||
ローカル変数0を PUSH |
定数No45を PUSH |
オブジェクトを 生成してPUSH |
スタックの最上位を 複製してPUSH |
コンストラクター 呼び出し (引数分をPOP) |
メソッドを呼び出し (引数分をPOPし 戻り値をPUSH) |
スタックからPOPして ローカル変数1へ |
|||
命令実行後の スタック の状態 |
Object | ||||||||
Object | Object | Object | |||||||
"メッセージ" | "メッセージ" | "メッセージ" | "メッセージ" | ||||||
this | this | this | this | this | 戻り値 | ||||
命令実行後の ローカル変数 の状態 |
2 | ||||||||
1 | 戻り値 | ||||||||
0 | this | this | this | this | this | this | this | this |
Javaソースのnewはオブジェクトのメモリ確保(new)とコンストラクター呼び出し(invoke special)の2段階になる。
invoke(メソッド呼び出し)では、引数の個数分だけスタックから値を取り出す(POP)。
invoke specialやinvoke
virtualでは暗黙の第1引数に“対象となるオブジェクト”を渡す必要があるので、Javaソース上の引数の個数より1つ多く見える。
メソッド呼び出し後は、
もし戻り値のあるメソッドなら、その戻り値がスタックに積まれる。
コンストラクター呼び出しは、バイトコードのレベルでは「戻り値の無いメソッド呼び出し」とほぼ変わり無い。
レジスターに相当するものが無いので、ローカル変数同士の演算もいちいちスタックに置いてからやっているのはちょっとショック(苦笑)
一応、若い番号に関しては専用命令を設けたりして、多少は効率よくなるよう考えられてはいるみたいだけど。
文字列やクラス名・メソッド名等の定数に関しては 徹底的に定数エリア(コンスタントプール)を使っているので、ルールが単純ということになり分かり易い。
コンパイルされたバイトコードは、ほぼJavaソースを忠実になぞっている。assertなんかはif文とthrowに変換されたりfor-eachはiteratorに展開されたりするけど。
つまり、効率の悪いコーディングは効率が悪いままになる。
ただ、最近はJIT(Just In Time)コンパイラ=実行時に最適化してくれるコンパイラがかなり頑張ってくれるらしいので、細かい最適化に関してはソース上で技巧に走るよりも、プログラマーが見て分かり易くなるように(=メンテナンスしやすいように)心がけるべきだろう。