先月のJJUGナイトセミナーで、Class-File APIとその前提知識となるバイトコードについてプレゼンしてきました。資料はこちら。
おかげさまで、コロナ後のオフラインに戻してから初のキャンセル待ちとなるぐらい盛況でした。もっともバイトコードの話よりも、増田さんのアーキテクチャーの話の方が期待されていたとは思いますけど。
まぁ、それはそれとして、バイトコードがどのように実行されるかというところから、Class-File APIまでを50分で説明するのは分量的になかなか難しく、特にClass-File APIの方はかなりはしょった説明になってしまいました。
そういえば、今までJava in the Boxでもバイトコードに関して触れたことがなかったので、いい機会ですし、バイトコードって何というところから説明していきたいと思います。
バイトコードって何?
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つの情報が記録されています。
- クラスファイルの情報
- クラスの情報
- コンスタントプール
- アトリビュート
クラスファイルの情報にはクラスファイルのバージョンなどが含まれます。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オプション
次回は、バイトコードの詳説に移りたいところですが、その前の事前知識としてスタックマシンについて紹介する予定です。