Sun Microsystems社より公開されているJVMの公式仕様『Java仮想マシン仕様(The Java Virtual Machine Specification)』(以下、JVM仕様)には、JVMで利用可能なバイトコードの仕様が説明されています。
JVM仕様では、バイトコードの命令列は、人間が読み取ることができるアルファベットの命令名(ニーモニック)と、その数値表現である1byteのオペコードで構成されています。クラスファイルはオペコードのみで構成されており、単なるバイナリデータです。試しにクラスファイルをテキストエディタで開いてみても、画像などのバイナリデータと同様に、文字にならないデータが表示されるだけです(クラスやフィールド、メソッドの名前は、バイトコード内にそのまま文字列として保存されているので、そこだけは何とか読み取ることができます)。
それでは、生成されたクラスファイルの中身を知ることはできないのでしょうか? 実は、あまり知られていませんが、そのような目的のためのツールが、JavaのSDKにデフォルトで用意されているのです。そのツールは「javap」というコマンドで、このコマンドを利用すればクラスファイルを人間が読み取れる形式に変換することができます。
javapコマンドは、引数にクラス名を渡して実行します。クラスファイルそのもの(つまり、「.class」拡張子が付いたファイルのパス)を渡すのではないことに注意してください。javapコマンドで指定できる主なオプションは、以下のとおりです。
オプション | 説明 | |
---|---|---|
-c | クラスファイルを逆アセンブルし、すべての命令列を表示する | |
-classpath <クラスパスのリスト> | クラスパスを指定する | |
-extdirs <フォルダ> | 拡張フォルダの場所を変更する | |
-help | ヘルプを表示する | |
-l | 行番号とローカル変数テーブルを出力する | |
-public | publicなクラスとメンバ(注)を表示する | |
-protected | protected/publicなクラスとメンバを表示する | |
-package | private以外のクラスとメンバを表示する(デフォルト) | |
-private | すべてのクラスとメンバを表示する | |
-s | クラスファイル内で用いられる型シグネチャを出力する | |
-verbose | メソッド内のスタックサイズ、ローカル変数と引数の個数を出力する | |
注:Javaにおける「メンバ」とは、そのクラス内で宣言されているフィールド、メソッド、内部クラス/インタフェースの総称です。 |
コマンドの使用法は、以下のとおりです。
> javap <オプション> <クラス名>
それでは、簡単なサンプルクラスファイルを利用して試してみます。以下のソースコード「HelloWorld.java」をコンパイルして、生成されたクラスファイルを読むことにします。
package net.mogra.wings.javatips; public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } }
-cオプションを付けてjavapコマンドを実行すると、次のような結果が標準出力に表示されます(左端にある行番号は説明のために付加したもので、実際の出力には付いていません)。
> javac net\mogra\wings\javatips\HelloWorld.java > javap -c net.mogra.wings.javatips.HelloWorld Compiled from "HelloWorld.java" public class net.mogra.wings.javatips.HelloWorld extends java.lang.Object{ public net.mogra.wings.javatips.HelloWorld(); Code: 0: aload_0 1: i nvokespecial #9; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #21; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #23; //String Hello World! 5: invokevirtual #29; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
この逆アセンブルされたクラスファイルについて、簡単に説明しておきます。
まず、3行目ではそのクラスファイルが作られた、元のソースコードのファイル名が表示されます。4行目は、そのクラスの宣言です。5〜9行目は、コンストラクタの命令列です。ソースコード「HelloWorld.java」にはコンストラクタはありませんでしたが、コンストラクタが省略された場合でも、バイトコードへコンパイルされるときにデフォルトのコンストラクタが挿入されるようになっています。コンストラクタの中では、インデックス0のローカル変数を読み込み(aload_0)、Objectクラスの初期化メソッドを呼び出し(invokespecial #9)、voidを返してメソッドを終了しています(return)。
11〜16行目が、ソースコードで宣言したmainメソッドの命令列になります。まず、Systemクラスのoutフィールドを読み込み(getstatic #21)、続いてStringオブジェクトの“Hello World!”を読み込んでいます(ldc #23)。その文字列を引数にしてprintlnメソッドを呼び出し(invokevirtual #29)、最後にvoidを返してメソッドを終了しています(return)。
このように、javapコンパイラを利用すれば、コンパイルされたクラスファイルの命令列を読み取ることができます。勉強やデバッグに利用してみてください。
Copyright © ITmedia, Inc. All Rights Reserved.