第4回:実機に載せよう(ダウンサイズ)
クラスファイルを覗こう
ツールを使用して説明用のクラス Temp のクラスファイルを覗いて見ます。BCEL( The Byte Code Engineering Library )というライブラリがあります。 BCEL は 簡単にクラスファイルの構造解析や変更ができます。 The Jakarta Projectよりダウンロードできます。
Class2HTML の使い方
BCEL には、クラスファイルの内容を HTML に変換する機能があります。
■ コマンドの形式
java -classpath bcel.jar org.apache.bcel.util.Class2HTML [-d 出力ディレクトリ名] [-zip JARファイル名] クラス名 (クラス名は複数可、スペースで分ける)
■ 例
java -classpath bcel.jar org.apache.bcel.util.Class2HTML -d ../html Temp.class
生成されたページの説明
上記のコマンドで html ディレクトリ内に生成されたTemp.htmlをみてみます。
フィールド&メソッド
private static final int field1 = 0 private static int field2 private int field3
ソースファイルで宣言したものがクラスファイル中にもそのまま入っています。ここで field1 は0に初期化されているように見えますが、これは ConstantValue 属性がついていることを示しています。
メソッドは以下のようになっています。
public void <init> () public static void main (String[]) private void method1 () private int method2 () static void <clinit> ()
ここで宣言していない名前のメソッドが二つほどあります。 <init> メソッドは、コンストラクタです。ソース上の public Temp() にあたります。 <clinit> は static 初期化節( static ブロック)メソッドで、
static{ ・・・ }
と書くブロックのメソッドです。
今回のソースに上記のような static ブロックはありませんが、 フィールドの宣言部分で
private static final int field1 = 0; private staticint field2 = 1;
と初期化も行っていますが、この初期化を static ブロックで行ったことになっています。 つまり、上記ソースが
private static final int field1; private staticint field2; static{ field1=0; field2=1; }
と解釈されています。ソースコードでフィールドの宣言と同時に初期化を書いても、バイトコードでは初期化はメソッドに移されます。
それでは、
privateint field3 = 2;
この初期化はどこに行ったのか、といいますと、<init>メソッド(=コンストラクタ)に移っています。
バイトコード
一番上に public void <init>() とあるように、<init> の詳細です。 Attributes とあるのは、そのメソッドについている属性です。 <init> には Code 属性が、そしてその Code 属性には LineNumberTable と LocalVariableTable 属性がついています。 Code 属性はソース上でメソッド内に書いたコードがインストラクションに翻訳されたバイトコードが入っています。 LineNumberTable はそのバイトコードとソース上の行番号の対応がかかれています。 LocalVariableTable は、メソッド内で宣言した変数の名前などを保持したものです。 LineNumberTable と LocalVariableTable は主にデバッグ時に用いられ、実行時には不必要な属性です。
バイトコードをみてみます。 ソース上では中身は空っぽだったコンストラクタですが、六つの命令が入っています。
Byte offset Instruction Argument 0 aload_0 1 invokespecial Object.<init> ()V():void 4 aload_0 5 iconst_2 6 putfield field3 I 9 return
図4 ローカル変数とオペランドスタックの遷移
図4 にこれらのコード実行時のローカル変数とオペランドスタックの遷移を示します。
最初の命令 "aload_0" は、0番目のローカル変数からオブジェクト参照を取り出してをスタック上に積む、というものです。 インスタンスメソッドの場合、メソッドが呼ばれたときの最初のローカル変数には、 0番目に this ポインタ、以降は引数が順に入っていきます。 コンストラクタもインスタンスメソッドでして、0番目には this ポインタが入っていますので、 this ポインタをスタック上に積んだことになります。 "invokespecial" は特定のインスタンスメソッドを呼び出す命令で、 ここでは Object.<init> を呼び出します。 この呼び出しで、先ほどスタックに積んだ this ポインタは降ろされます。 "aload_0" は最初の命令と同じで、スタックに this ポインタを再度積みます。 "iconst_2" は integer の2をスタックに積みます。ここでスタックは this ポインタ、integer の2、となります。 "putfield" はインスタンスフィールドに値を入れます。 この実行で先ほどスタックに積んだ this ポインタの field3 に integer の2を入れます。 そしてスタックは空になります。 最後に"return"でメソッド終了です。 |
private void method1(){ int variable = field1+method2(); System.out.println("method1:"+variable); }
また、ソースコードでは上記のように method1 の中で field1 を参照していますが、バイトコード中では field1 への参照はありません。 つまり ConstantValue がついているフィールドはコンパイラによって値が展開されます。 (展開されたあと、残ったフィールドはサイズを増やすだけで、無駄にしかなりません。ソースコードで定数を展開するとサイズが削減できるのは、 この余分なフィールドが残らないためです。)
コンスタントプール
再度 <init> メソッドのバイトコードに戻ります。
3列目の Argument の項は表示上展開されていますが、バイトコード中ではコンスタントプールへの参照です。
"invokespecial" の Argument "Object.<init> ()V"の"<init> ()V" をクリックしてください。 左上のコンスタントプール一覧に出てくる 1 CONSTANT_Methodref が参照されていることがわかります。 "putfield" の Argument "field3 I" も2番目のコンスタントプールエントリ CONSTANT_Fieldref を参照しているのですが、残念ながら HTML からはわかりません。
表2で示したコンスタントプールの参照関係をこの "Object.<init> ()V" を例で具体的に見てみましょう。
- エントリ1 は Methodref、メソッドのクラスとしてエントリ16、名前と型としてエントリ35を参照しています。
- エントリ16は Class 、クラス名を表す エントリ50を参照しています。
- エントリ50は Utf8、"java/lang/Object" という文字列を保持しています。
- エントリ35は NameAndType、 名前と型を表す二つのCPエントリ23、24を参照します。
- エントリ23は Utf8、"<init> "という文字列を保持しています。
- エントリ24も Utf8、"()V"という文字列を保持しています。
- エントリ1が依存するエントリがこれだけありました。図5 にこの関係図をしめします。
図5 コンスタントプールエントリの参照関係例
このようにデータ領域であるコンスタントプールを使うことによって、"0xB7 0x00 0x01" という3バイトで、 "invokespecial Object.<init> ()V" の呼び出しが可能となっています。 (invokespecial のオペコードは 0xB7)
属性
説明していない属性をみてみましょう。 1番の ConstantValue 属性は、フィールドの field1 についています。static final で定数の時につくものです。 16番の SourceFile 属性はクラス自体につく属性で、ソースファイル名を表します。
属性には「名前」があり、データ構造や内容は属性ごとに決まっています。 Code 属性では Java のバイトコード以外の多くの属性は実行時には不要です。コンパイルオプション -g:none やインストラクションの最適化でサイズ削減できます。
クラスファイルの変更
ソースに変更を加えて、クラスファイルのサイズを変えるのは有効ですが、限界があります。
Class Construction Kit
BCEL には、Class Construction Kit というツールも付属しています。クラスファイルをソースなしに直接生成したり、クラスファイルを編集するツールです。
次のメソッドをダウンサイズします。
private void connect(){ HttpConnection http = null; DataInputStream input = null; int result; String temp = "/"; try { http = (HttpConnection)Connector.open(temp); input = http.openDataInputStream(); result = input.readInt(); } catch (IOException ioe) { } try { if (input != null) { input.close(); } if (http != null) { http.close(); } } catch (IOException ioe) { } }
通信するメソッドとしてはよくある形をしています。 これをバイトコードレベルで見ると以下のようになります。
private void connect() Attributes Code LineNumberTable LocalVariableTable Byte offset Instruction Argument 0 aconst_null 1 astore_1 2 aconst_null 3 astore_2 4 ldc "/" 6 astore %4 8 aload %4 10 invokestatic javax.microedition.io.Connector.open (Ljava/lang/String;)Ljavax/microedition/io/Connection; (String):javax.microedition.io.Connection 13 checkcast javax.microedition.io.HttpConnection 16 astore_1 17 aload_1 18 invokeinterface javax.microedition.io.InputConnection.openDataInputStream ()Ljava/io/DataInputStream;():java.io.DataInputStream 23 astore_2 24 aload_2 25 invokevirtual java.io.DataInputStream.readInt ()I():int 28 istore_3 29 goto 37 32 astore %5 34 goto 37 37 aload_2 38 ifnull 48 41 aload_2 42 invokevirtual java.io.DataInputStream.close ()V():void 45 goto 48 48 aload_1 49 ifnull 66 52 aload_1 53 invokeinterface javax.microedition.io.Connection.close ()V():void 58 goto 66 61 astore %5 63 goto 66 66 return 4 Code Maximum stack size = 1 Number of local variables = 6 Byte code Exceptions handled java.io.IOException (Ranging from lines 8 to 29, handled at line 32) java.io.IOException (Ranging from lines 37 to 61, handled at line 61)
インストラクションを変更してメソッドを短くする方法の例です。
- goto へ飛ぶ goto の、目的オフセット書き換え(短縮化にはならない)
- return へ飛ぶ goto の、return への置き換え
- 不必要なローカル変数への保存を削除
インストラクションのオフセット 34 を見てください。 goto 37 となっていますが、オフセット 37 は次の行です。オフセット 34 を削除するには他から参照されていなか確認しなければいけません。オフセット 63 の goto も同様です。
オフセット 58 の goto は、return に飛びます。この goto を return に置き換えることで、3 バイトの goto が 1 バイトの return になります。
オフセット 32 の astore 5 です。ローカル変数の 5 番目にスタック上の値を積む、という命令です。さて、このメソッド内を見渡して、このローカル変数 5 番目の値を使用する個所があるでしょうか?実際には使われません。pop命令に置き換えてスタック上の値を捨ても構いません。オフセット 63 の astore 5 も同様です。
オフセット 6・8 のペアを見てください。astore 4 & aload 4 というこのペアは、スタックにある値を 4 番目のローカル変数に格納し(astore 4)、 そのローカル変数から値をスタックに積んでいます( aload 4 )。4 番目のローカル変数は使われませんので、オフセット 6・8 を消して構いません。4 番目のローカル変数が無くなるので、5 番目以降のローカル変数の添え字をずらす必要があります。
実際に Class Construction Kit を使ってダウンサイズします。 Class Construction Kit を用いるためには、まず BCEL に付属の CCK.jar を bcel.jar と同じフォルダに置きます。 そして CCK.jar を起動してください。CCK.jar をダブルクリック、もしくは以下のコマンドを実行してください。
java -jar CCK.jar
メニューの File から open で編集したいクラスファイルを選択します。 左ウインドウのメソッド一覧から connect の Code を選んでください。インストラクション一覧が右ウインドウに出ます。( 図 4 )
消したい行をクリックして右に並ぶボタンの Remove を押せば消えますが、LineNumberGen がどうのこうの、というエラーダイアログが出ます。「この行を参照するものがあった」、ということです。メソッドの属性の LineNumberTable が参照しています。 LineNumberTable はデバッグ情報なので不要です。 左ウインドウのメソッド一覧から connect の LineNumberTable を選び、右ウインドウの項目全てを消せば完了です。 これで Instruction が削除できます。
編集後のインストラクション列は次のようになります。
private void connect() Attributes Code Byte offset Instruction Argument 0 aconst_null 1 astore_1 2 aconst_null 3 astore_2 4 ldc "/" 6 invokestatic javax.microedition.io.Connector.open (Ljava/lang/String;) Ljavax/microedition/io/Connection; (String):javax.microedition.io.Connection 9 checkcast javax.microedition.io.HttpConnection 12 astore_1 13 aload_1 14 invokeinterface javax.microedition.io.InputConnection.openDataInputStream ()Ljava/io/DataInputStream;():java.io.DataInputStream 19 astore_2 20 aload_2 21 invokevirtual java.io.DataInputStream.readInt ()I():int 24 istore_3 25 goto 29 28 pop 29 aload_2 30 ifnull 37 33 aload_2 34 invokevirtual java.io.DataInputStream.close ()V():void 37 aload_1 38 ifnull 49 41 aload_1 42 invokeinterface javax.microedition.io.Connection.close ()V():void 47 return 48 pop 49 return 4 Code Maximum stack size = 1 Number of local variables = 4 Byte code Exceptions handled java.io.IOException (Ranging from lines 6 to 25, handled at line 28) java.io.IOException (Ranging from lines 29 to 48, handled at line 48)