Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

クラスファイルはすべからくjavapしよう

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

javac HelloWorld.javaしてクラスファイルを生成します。さっそくjavap HelloWorldしましょう。javapコマンドはOpenJDK系(OpenJDKやHotSpot、JRockit)に付属するので、たとえばIBM J9などにはありません。ご注意ください。以下のように出力されます(JDK8にて実施)。

Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
  public static void main(java.lang.String[]);
}

クラスファイルを逆アセンブルして、クラス名やコンストラクタメソッドを出力しました。javap --helpしてオプションを見てみましょう。

-help
使用方法: javap <options> <classes>
使用可能なオプションには次のものがあります:
  -help  --help  -?        この使用方法のメッセージを出力する
  -version                 バージョン情報
  -v  -verbose             追加情報を出力する
  -l                       行番号とローカル変数表を出力する
  -public                  publicクラスおよびメンバーのみを表示する
  -protected               protected/publicクラスおよびメンバーのみを表示する
  -package                 package/protected/publicクラスおよび
                           メンバーのみを表示する(デフォルト)
  -p  -private             すべてのクラスとメンバーを表示する
  -c                       コードを逆アセンブルする
  -s                       内部タイプ署名を出力する
  -sysinfo                 処理しているクラスのシステム情報(パス、サイズ、日付、MD5ハッシュ)
                           を表示する
  -constants               final定数を表示する
  -classpath <path>        ユーザー・クラス・ファイルを検索する場所を指定する
  -cp <path>               ユーザー・クラス・ファイルを検索する場所を指定する
  -bootclasspath <path>    ブートストラップ・クラス・ファイルの場所をオーバーライドする

このエントリでは、さまざまなオプションでの出力を確認することとします。 まず-versionつけてみます。

$ javap -version                                                                                                                                                                                                                                          1.8.0_60

これはjavapがどのJDKバージョンのものかを確認するものです。とくに逆アセンブル結果の出力には影響しません。 次は-v/-verboseしてみます。

-v/-verbose
$ javap -v HelloWorld                                                                                                                                                                                                                                     
Classfile /Users/jyukutyo/temp/HelloWorld.class
  Last modified 2015/08/19; size 425 bytes
  MD5 checksum 63e47f1d243e0eb6bc952df3f6ac0d5a
  Compiled from "HelloWorld.java"
public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // HelloWorld
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               HelloWorld.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               HelloWorld
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
}
SourceFile: "HelloWorld.java"

javapするとき一番よく指定するオプションだと個人的に思います。クラスに関する詳細な情報を見ることができます。ファイルサイズやチェックサムコンパイルしたバージョンやコンスタントプールも見れます。各メソッドについてもJVMバイトコード命令も表示されます。ここを解説し出すととても長い文章になるので、今回は深入りしないこととします。次はアクセス修飾子の制限をつけてみます。-privateにします。

-private
$ javap -private HelloWorld
Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
  public static void main(java.lang.String[]);
}

おや、publicなものも出ていますね。-private-protectedといったオプションは、そのアクセス修飾子のものだけ出力するのではなく、そのアクセス修飾子以上の可視性があるものをすべて出します。なので-privateとすると、すべてのメソッドとメンバーを出力します。-cを使ってみます。

-c
$ javap -c HelloWorld
Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

JVMバイトコード命令が出ました。-vを使ったときにも含まれる内容です。バイトコード命令だけを見るときは便利そうです。次は-sを使ってみます。

-s
$ javap -s HelloWorld
Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
}

-sは内部タイプ署名を出すとありますが、メソッドシグネチャなのかな?引数及び戻り値の型がわかります。次-sysinfo

-sysinfo
$ javap -sysinfo HelloWorld                                                                                                                                                                                                                               Classfile /Users/jyukutyo/temp/HelloWorld.class
  Last modified 2015/08/19; size 425 bytes
  MD5 checksum 63e47f1d243e0eb6bc952df3f6ac0d5a
  Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
  public static void main(java.lang.String[]);
}

変更日付、コンパイルした日付でしょうね。ファイルサイズやMD5チェックサムが出ました。-sysinfoはJDK7から追加されたもののようです。JDK7でクラスファイルのフォーマット変更があったはずなので、それで追加された情報を出せるようにしたのかもです。

-bootclasspathも同様にJDK7からです。7で新たに追加されたJVM命令「invokedynamic(indy)」のためです。indyを使うにはBootstrapクラスを必要とします。JDK8で導入されたラムダではjava.lang.invoke.LambdaMetafactoryが該当します(ラムダの場合はJavaに組み込まれていますから、このオプションで指定する必要はないですが)。

まとめ

クラスファイルを見たらjavap -vしよう!新しい言語仕様が追加されたら、コードを書いてjavap -vしよう!

JDK7ですが、javapコマンドの仕様はこちらです(日本語)。 http://docs.oracle.com/javase/jp/7/technotes/tools/windows/javap.html