2025/03/06

バイトコード入門 その3 バイトコード処理の構成

このエントリーをはてなブックマークに追加

ちょっと間があいてしまいましたが、バイトコード入門の3回目です。今回はバイトコードをどのように処理してJVMの構成について紹介します。前回のスタックマシンがここで活かされてきます。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成 (今回)

 

JVMのメモリ構成

JVMで管理しているメモリ領域というと、多くの方がヒープと考えるのではないでしょうか。たしかに、ヒープは重要なメモリ領域であることには違いはありませんが、ヒープ以外にもJVMが管理しているメモリ領域があるのです。

HotSpot VMの場合、JVMが管理しているメモリ領域は大別して4種類あります。

  • ヒープ: オブジェクトの配置用の領域
  • メタスペース: クラス定義、メソッド定義、コンスタントプールなど
  • ネイティブメソッドスタック: ネイティブメソッド用領域
  • JVMスタック: コールスタック用領域

JVMSはJVMの仕様については記述されていますが、実装については記述されません。メモリ構成も実装に近いため大まかにしか説明されていませんが、JVMS 2.5 Run-Time Data Areasに記述があります。

なお、下図はJVMスタックを省略しています。

 

1つ目のヒープは、一番なじみがあるメモリ領域だと思います。

Javaのオブジェクトは必ずこのヒープに配置されます。GCの種類によってヒープはさらに細分化されるのですが、それはGCによるものでJVMSでは定義されていません。

たとえば、世代別GCを使用している場合、Young領域とOld領域に分けられるなどがこれに相当します。

2番目のメタスペースは静的なデータを保持させる領域で、クラスローダーごとに管理されています。

メタスペースというのはHotSpot VMの実装に基づいた領域名で、メタスペースに保存するデータとしてはクラス定義、メソッド定義などがあります。また、コンスタントプールもメタスペースに保持されます。

JVMSでは2.5.4 Method Area2.5.5 Run-Time Constant Poolがメタスペースの一部になっています。

ネイティブメソッドスタックは、JNIやFFMでネイティブメソッドを使用する際に使われる領域です。

最後のJVMスタック領域が今回メインで取り上げるメモリ領域です。JVMSでは2.5.2 Java Virtual Machine Stacksで定義されています。

これ以外にスレッドごとにどこを実行しているかを保持しておくPCレジスタ(Program Counter)もあります(JVMS 2.5.1)。

 

JVMスタック領域

例外がスローされた時に出力されるスタックトレースはJavaの開発者であれば誰もが見たことがあるはずですが、その意味を考えたことがありますか?

何かのスタックをトレースしたものということは分かると思います。そのスタックというのは、メソッドがコールされた順序を保持しておくスタックを指しています。一般的にはコールスタックと呼ばれるスタックです。

Javaの場合、このコールスタックはJVMスタックと呼ばれます。そして、そのJVMスタックが配置される領域がJVMスタック領域です。

なお、コールスタックが使用されていても、前回紹介したスタックマシンとは呼ばれないことに注意が必要です。

さて、JavaのJVMスタックは、並列処理が可能なようにスレッドごとに作られます。

そして、JVMスタックに積まれるのがフレームです(まちがえないとは思いますが、AWTのjava.awt.Frameクラスではないです)。

フレームはメソッドコールごとにJVMスタックに積まれます。たとえば、以下のようにmainからfoo、barとコールされる場合を考えてみましょう。

public class Main {
    static void bar() {}

    static void foo() {	bar(); }
    
    public static void main(String... args) {
	foo();
    }
}

Mainクラスを実行すると、まずmainに対応するフレームが積まれます。fooメソッドがコールされるとそれに対応するフレームが積まれます。

そして、fooメソッドからbarメソッドがコールされると、barメソッドに対応するフレームが積まれます。

barメソッドが完了して、fooメソッドに戻る時に、barメソッドに対応するフレームは削除されます。

同様にfooメソッドの完了時にfooメソッドに対応するフレームが削除され、mainメソッドが完了する時にmainメソッドに対応するフレームが削除され、JVMスタックは空になります。

 

ところで、StackOverflowErrorという例外に遭遇したことがあるでしょうか。再帰などでコードにバグがある時に遭遇することが多い例外です。

再帰では自分自身を延々とメソッドコールするわけですが、メソッドコールの連なりが限度を超えた場合にStackOverflowError例外がスローされます。

もうお分かりだとは思いますが、StackOverflowError例外のスタックとはJVMスタックのことです。

JVMスタックにフレームを積み過ぎてあふれてしまうと、StackOverflowError例外がスローされるのです。

同様に例外発生時に提示されるスタックトレースのスタックもJVMスタックです。

例外発生時に、どのメソッドのコールされていたかは、JVMスタックをたどれば分かります。これがスタックトレースです。

実際には、次節で紹介するJVMスタックに積まれるフレームの情報を含めてスタックトレースが作られます。

 

フレーム

JVMスタックに積まれるフレームはJVMS 2.6 Framesで定義されています。

フレームの主要な構成要素は以下の2つです。

  • ローカル変数用配列
  • オペランドスタック

 

ローカル変数用配列

1つ目のローカル変数用配列は、ローカル変数とメソッドの引数を保持させる配列です。

ローカル変数およびメソッド引数がいくつ使用するのかは、ソースコードをコンパイルする時に調べることができます。このため、配列の要素数はその個数分になります。

ローカル変数もしくは引数がプリミティブ型の場合、その値が直接配列に保持されます。参照型の場合はその参照が保持されます。

また、インスタンスメソッドの場合、インデックス0には必ずthisが入ります。

 

とこで、ローカル変数/メソッド引数の名前はコンパイルすると情報として残りません。メソッド内では、ローカル変数用配列のインデックスで指定されます。

しかし、これだとクラスファイルを読んだだけだと何が配列に入っているのかが分かりにくいんですよね。

こんな理解度の低い人間のためのことを、javacはちゃんと用意してくれてあります。それがコンパイルオプションの-gです。

-gはデバッグ情報をクラスファイルに埋め込むためのオプションです。

たとえば、次のメソッドで試してみましょう。

 

    void sayHello(String name) {
        var text = "Hello, " + name + "!";
        System.out.println(text);
    }

 

これを-gを使用せずにコンパイルし、javap -vで表示させると次のようになります。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14

 

赤字で示したlocal=3がローカル変数用配列のサイズになります。

そして、次回、詳しく説明しますが、aload_1やastore_2の1や2が配列のインデックスになります。

しかし、そのインデックス1や2に何が保持されているかは、バイトコードから推測するしかありません。

 

そこで-gを使用してコンパイルしてみます。コンパイル結果を次に示します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LHello;
            0      15     1  name   Ljava/lang/String;
            7       8     2  text   Ljava/lang/String;

 

-gオプションを使用することで、最後にLocalVariableTableという表が追加されました。

この表のSlotが配列のインデックスになります。

sayHelloメソッドはインスタンスメソッドなので、前述したようにインデックス0にはthisが入ります。

インデックス1には引数のname、インデックス2にはローカル変数のtextです。

 

クラスファイルを読み慣れてくれば、Local Variable Tableがなくても、配列のどこに何が保持されているかは分かってきます。しかし、慣れないうちは、コンパイルオプションの-gをつけてコンパイルすることをお勧めします。

 

オペランドスタック

フレームのもう一方の構成要素がオペランドスタックです。名前の通り、オペランドを保持させるスタックです。

もちろん、このスタックがJVMをスタックマシンたらしめているスタックです。

オペランドというのは、処理命令であるオペコードの処理対象のデータのことになります。

バイトコードにはloadやstoreといったオペコードがあります。これらはローカル変数用配列からスタックにデータを積む、スタックの先頭データを取り出してローカル変数用配列に保持させるというようにスタックに対する処理になります。

他のバイトコードもほとんどがスタックやスタックのデータに対する命令となります。

 

このスタックのサイズも、コンパイル時に決まります。

先ほどのsayHelloメソッドの場合を見てみましょう。sayHelloメソッドの先頭部分を再掲します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2

このstack=2がオペランドスタックのサイズとなります。

 

ちなみに、args_size、つまり引数の個数が2になっているのは、インスタンスメソッドの暗黙の引数としてthisが渡されるからです。

 

バイトコード処理構成のまとめ

長くなってきたので、今回はここまでとして、まとめてみましょう。

  • JVMのメモリ領域のうち、バイトコード処理に使われるのはJVMスタック領域
  • JVMスタックはスレッドごとに作成され、メソッドコールごとにフレームが積まれる
  • フレームはローカル変数用配列とオペランドスタックなどから構成される
  • オペランドスタックを使用してバイトコードの処理を行う

 

たぶん、次のエントリーはJEPで語れないシリーズのJava 24になると思うので、その後のエントリーでやっとバイトコード処理について解説できるはずです。

2025/02/12

バイトコード入門 その2 スタックマシン

このエントリーをはてなブックマークに追加

前回からはじめたバイトコード入門。なかなかバイトコードに入れなくても申し訳ないのですが、今回はスタックマシンについて紹介します。

  1. 準備編 (前回)
  2. スタックマシン (今回)
  3. バイトコード処理の構成

 

スタックマシン

スタックマシンというのは計算モデルの1つです。

スタックマシンと、よく引き合いに出される計算モデルにレジスタマシンがあります。

この2つの計算モデルはメモリーの使い方にあります。

  • スタックマシン: メモリーをスタックとする計算モデル
  • レジスタマシン: メモリーをレジスタとする計算モデル

純粋なスタックマシンはスタックだけで構成しますが、通常はランダムアクセスができるメモリーと組み合わせで使われることが多いです。これはレジスタマシンでも同様で、通常はレジスタ以外にメモリーを使用します。

スタックマシンにはJVMの他にも.NET Frameworkでも使用されています。

一方、既存のほとんどのCPUがレジスターマシンになります。

たとえば、レジスターマシンで加算を行う場合、aレジスタの値とbレジスタの値を加算して結果をaレジスタに格納するというような命令になります(add a, bのような感じです)。

命令の対象が明確に記述されているので、分かりやすいはずです(とはいうものの、機械語でプログラムを書けと言われてもイヤですけど😰)

では、スタックマシンはどのように動作するのでしょう?

ここでは、単純な例としてHPの電卓でスタックマシンの動作を説明していきます。

 

HPの電卓

HPというと、紆余曲折あってPCやプリンターのHP Inc.とサーバーのHPEになっていますが、かつては計測機器を扱う会社でした。そんなHPが1970年代から2000年代にかけて電卓を作っていたのでした。

当時は、関数電卓やプログラム電卓といったらHPという感じで、そこそこ使われていました。プログラム電卓というのは、その名の通りプログラムが組める電卓です。

このHPの電卓は、なんといっても入力方式が独特でした。

たとえば、1+2を計算する場合、通常の電卓であれば 1 + 2 = と入力しますね。これに対し、HPの電卓は 1 [Enter] 2 [Enter] + と入力しました([Enter]というボタンがあったのです)。

[Enter]を省略して記述すると 1 + 2 は 1 2 + になるということです。この数式の書き方は逆ポーランド記法と呼ばれています。

 

逆ポーランド記法

ちょっと脱線気味ですが、逆ポーランド記法についても説明しておきましょう。

「逆」とついていることから分かるかもしれませんが、「逆」ではないポーランド記法もあります。というか、こちらが先ですね。

ポーランド記法はJan Łukasiewicz (ヤン・ウカシェヴィチ)が発案した数式の記法です。

演算子を先に記述することから前置記法とも呼ばれます。

私たちが通常使用している 1 + 1 のような記法は演算子が数値の間に記述されるので中置記法と呼びます。逆ポーランド記法は演算子が最後なので、後置記法になります。

 

逆ポーランド記法の利点はカッコを使用せずに数式を記述できるところです。

たとえば、以下の数式はどうでしょう。

(2 + 3) × 5 + (4 - 2) ÷ 2

これを逆ポーランド記法で記述すると以下のようになります。

2 3 + 5 × 4 2 - 2 ÷ +

カッコがないというのは、電卓でメモリ機能(M+やM-、MRCなど)を使わなくても計算ができるということで、入力が簡単になります。まぁ、逆ポーランド記法で考えなければいけないというハードルはありますけど。

そして、もう1つの利点が、逆ポーランド記法はスタックを使えば簡単に実装できるということです。

 

スタックを使用した逆ポーランド記法の計算

では、逆ポーランド記法の数式をスタックを使用して計算してみましょう。

ルールは簡単です。

  1. 数値であれば(HPの電卓では、[Enter]が入力されたら)その値をスタックに積む
  2. 演算子であれば、演算に必要な個数のデータをスタックから取り出し、計算結果を再びスタックに積む

では、1 + 2をやってみましょう。

 

電卓であれば、スタックの先頭を表示していれば計算結果が表示されます。

複雑な数式でも計算の途中結果がスタックに保持されているので、スタックとは別のメモリーを使用しなくても計算が実行できます。

 

電卓では計算だけですが、一般のスタックマシンでも同様に必要なデータをスタックに置き、処理の結果を再びスタックに置くという過程で処理が進みます。

これはJVMでも同様です。

では、次回は実際にJVMでどのようにスタックマシンが構成されているのか紹介する予定です。

2025/02/10

バイトコード入門 その1 準備編

このエントリーをはてなブックマークに追加

先月のJJUGナイトセミナーで、Class-File APIとその前提知識となるバイトコードについてプレゼンしてきました。資料はこちら。

 

おかげさまで、コロナ後のオフラインに戻してから初のキャンセル待ちとなるぐらい盛況でした。もっともバイトコードの話よりも、増田さんのアーキテクチャーの話の方が期待されていたとは思いますけど。

 

まぁ、それはそれとして、バイトコードがどのように実行されるかというところから、Class-File APIまでを50分で説明するのは分量的になかなか難しく、特にClass-File APIの方はかなりはしょった説明になってしまいました。

そういえば、今までJava in the Boxでもバイトコードに関して触れたことがなかったので、いい機会ですし、バイトコードって何というところから説明していきたいと思います。

  1. 準備編 (今回)
  2. スタックマシン
  3. バイトコード処理の構成

 

バイトコードって何?

Javaでシステムを作っているだけであれば、バイトコードに触れる機会はまずないはずです。

バイトコードというのはJava特有の言葉ということではなく、他にも使われる言葉です。

一般的には仮想マシンなどの実行環境が解釈するための中間表現です。人間が読むことを想定しておらず、バイナリーで記述されるため、バイトコードと呼ばれるようです。

Javaの場合、バイトコードは当然ながらJVMが解釈するために使用されます。

バイトコードは約200種類のJVMに対する命令(オペコードもしくはインストラクションと呼ばれます)からなっています。もちろん、バイナリーなので16進数表記で表されますが、さすがにそれでは読みにくいのでloadやstoreなど命令に1対1に対応づいた表記で表されることが多いです。

機械語に対しうるアセンブラのようなものですね。

具体的な命令などは次回以降に説明します。

 

バイトコードはどこに記述される?

Javaのソースコードをjavacでコンパイルすると、クラスファイルが生成されます。このクラスファイルにバイトコードが含まれています。

なお、クラスファイルにはバイトコード以外にも実行に必要な情報が含まれています。

バイトコードの定義や、クラスファイルのフォーマット定義などはJava Virtual Machine Specification (JVS)に記載されています。

Java言語の仕様であるJava Language Specification (JLS)は、Javaのバージョンごとに改定されていますが、JVMSも同じくバージョンごとに改定されます。

最新版のJava 23でのJVMSは以下のURLで参照できます。

クラスファイルのフォーマットは4章、バイトコードは6章に記載されています。

クラスファイルには大まかにいうと、以下の4つの情報が記録されています。

  1. クラスファイルの情報
  2. クラスの情報
  3. コンスタントプール
  4. アトリビュート

クラスファイルの情報にはクラスファイルのバージョンなどが含まれます。Java言語にもバージョンがありますが、クラスファイルにもバージョンがあるのです。

たとえば、Java 23のjavacでコンパイルされたクラスファイルのバージョンは67になります。

クラスの情報は、クラス名やスーパークラス、実装しているインタフェースなどです。コンスタントプールはクラスで使用される様々な定数を定義していあります。

最後のアトリビュートは様々な情報を記載することができ、バイトコードもそのうちの1つになります。

アトリビュートにはバイトコード以外にフィールド定義やメソッド定義などが記載されています。

 

javapコマンド

前述したようにバイトコードはバイナリで表されます。同様にクラスファイルも人が読むことを想定していないため、バイナリファイルです。

とはいうものの、クラスファイルに何が記述されているのか確認したいこともありますよね。

こういう時に使用するのが、JDKに含まれているjavapコマンドです。

javapコマンドはいろいろな情報が出せるので、さっそく試してみましょう。

サンプルに使うのはおなじみのHello, World!です。

public class Hello {
    static final String HELLO = "Hello, World!";

    private void sayHello() {
        System.out.println(HELLO);
    }
    
    public static void main(String... args) {
        new Hello().sayHello();
    }
}

 

まず、javacでコンパイルしてから、javapを実行します。javapの引数はクラス名もしくはクラスファイル名です。

> javac Hello.java

> javap Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;
  public Hello();
  public static void main(java.lang.String...);
}

>

 

何もオプションを指定せずにjavapを実行すると、クラスの情報と宣言されているフィールド、メソッドの情報が出力されます。

しかし、何か抜けているような気がしませんか。

そう、プライベートメソッドのsayHelloメソッドが抜けているのです。

javapはデフォルトでは、パッケージプライベートで宣言されたフィールド、メソッドしか出力しません。プライベートで宣言されたフィールド、メソッドを出力するには、オプションの-privateもしくは-pを指定します。

> javap -p Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;
  public Hello();
  private void sayHello();    
  public static void main(java.lang.String...);
}

>

 

この出力結果を見ると、Helloクラスはパブリッククラスで、フィールドはパッケージプライベートでstatic finalのHELLO、メソッドはデフォルトコンストラクター、sayHelloメソッド、そしてstaticメソッドのmainが宣言されていることが分かります。

元のHello.javaにはデフォルトコンストラクターは記述されていませんが、コンパイル時に自動生成されます。

 

バイトコードの解析: -cオプション

javapコマンドでバイトコードを出力するには、-cオプションを使用します。

> javap -p -c Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;

  public Hello();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  private void sayHello();
    Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String Hello, World!
         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public static void main(java.lang.String...);
    Code:
         0: new           #13                 // class Hello
         3: dup
         4: invokespecial #23                 // Method "<init>":()V
         7: invokevirtual #24                 // Method sayHello:()V
        10: return
}

>

 

この場合でも、-pオプションを使用しないとプライベートフィールド、メソッドは出力されないので、忘れないようにしましょう。

バイトコードはメソッド定義の後のCode:の次の行から始まります。aload_0やinvokespecialなどがバイトコードの命令です。

invokespecial #1のように、#と数字で表示されているのはコンスタントプールを指しています。#1はコンスタントプールのインデックス1の定数です。具体的にはコメントで示されているように、Objectクラスのデフォルトコンストラクタへのメソッド参照です。

なお、このコメントはjavapが解析結果を付記したもので、クラスファイルに記載されているわけではありません。

このコメントがあると、バイトコードやコンスタントプールを読む手間がかなり省けるはずです。

 

全部出力: -verbose/-vオプション

バイトコードだけでなく、コンスタントプールの値や、その他のアトリビュート、フラグなどの情報を出力するには-verboseもしくは-vオプションを使用します。

ちょっと長いですが、Helloクラスを-vでの出力を以下に示します。

 

> javap -p -v Hello
Classfile /temp/Hello.class
  Last modified 2025/02/09; size 557 bytes
  SHA-256 checksum 5b4893687f4d64e1e6a659d495f8febf0680a2a3d8a036c2e40c5057d2d0c5d1
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 68
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #13                         // Hello
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Class              #10            // java/lang/System
   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
  #10 = Utf8               java/lang/System
  #11 = Utf8               out
  #12 = Utf8               Ljava/io/PrintStream;
  #13 = Class              #14            // Hello
  #14 = Utf8               Hello
  #15 = String             #16            // Hello, World!
  #16 = Utf8               Hello, World!
  #17 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #18 = Class              #20            // java/io/PrintStream
  #19 = NameAndType        #21:#22        // println:(Ljava/lang/String;)V
  #20 = Utf8               java/io/PrintStream
  #21 = Utf8               println
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Methodref          #13.#3         // Hello."<init>":()V
  #24 = Methodref          #13.#25        // Hello.sayHello:()V
  #25 = NameAndType        #26:#6         // sayHello:()V
  #26 = Utf8               sayHello
  #27 = Utf8               HELLO
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               ConstantValue
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               SourceFile
  #35 = Utf8               Hello.java
{
  static final java.lang.String HELLO;
    descriptor: Ljava/lang/String;
    flags: (0x0018) ACC_STATIC, ACC_FINAL
    ConstantValue: String Hello, World!

  public Hello();
    descriptor: ()V
    flags: (0x0001) 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

  private void sayHello();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String Hello, World!
         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: new           #13                 // class Hello
         3: dup
         4: invokespecial #23                 // Method "<init>":()V
         7: invokevirtual #24                 // Method sayHello:()V
        10: return
      LineNumberTable:
        line 9: 0
        line 10: 10
}
SourceFile: "Hello.java"

>

 

Constant pool:で始まる次の行からがコンスタントプールです。

先ほど-cオプションで出力したバイトコードで#1が参照されていましたが、このコンスタントプールの表で見てみましょう。

#1の行にはメソッドの参照を示すMethodrefに続いて#2.#3と記載されています。

#2のClassはクラスの参照を示しており、その名前は#4に記載されいます。

#4の後のUtf8は文字列を表しています。#4で定義されている文字列定数はjava/lang/Object、つまり#2で参照していたクラス名です。

では、#1で参照しているもう1つの#3を見てみましょう。

#3はNameAndType、つまりメソッド名と型(ここではクラス名ではなく、メソッドのシグネチャーです)を示しており、#5と#6を参照しています。

#5は文字列定数で<init>、これはコンストラクタを表しています。#6も文字列定数で()Vです。これはメソッドのシグネチャーが引数なし、戻り値がvoidであることを示しています。

このように、コンスタントプールは参照、参照となっていますが、各々の行にコメントが追記されているので、これを見れば参照を追わずとも分かるはずです。

 

フィールドやクラスも追加の情報が記載されていることが分かると思いますが、これらに関してはバイトコードの詳細説明の時に触れる予定です。

 

準備編まとめ

  • バイトコードはJVMが解釈するための中間表記
  • クラスファイルにはバイトコード以外に実行に必要な情報が記載される
  • クラスファイルの解析コマンド javap
  • javapでプライベートを含めて出力: -pオプション
  • javapでバイトコードを出力: -cオプション
  • javapで全部出力: -vオプション

 

次回は、バイトコードの詳説に移りたいところですが、その前の事前知識としてスタックマシンについて紹介する予定です。