S-JIS[2007-11-11] 変更履歴

バイトコード

Javaのソースをコンパイルするとclassファイルが作られるが、その中身はバイトコードと呼ばれる中間形式。
JavaVMがそのバイトコードを解釈して実行する。

classファイルの中身には色々な情報が入っているが、当ページではその中のプログラムに関する部分をメモしている。


逆アセンブル方法

バイトコード(インストラクションコード?)はいわばアセンブリ言語のようなもので、javapjadを使うと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)コンパイラ=実行時に最適化してくれるコンパイラがかなり頑張ってくれるらしいので、細かい最適化に関してはソース上で技巧に走るよりも、プログラマーが見て分かり易くなるように(=メンテナンスしやすいように)心がけるべきだろう。


参考


Javaへ戻る / 技術メモへ戻る
メールの送信先:ひしだま