Dev Study
← 解説「ソースが動くまで: javac と .class と JVM」に戻る

サンプルコードで身につける: ソースが動くまで: javac と .class と JVM

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1最小の Java プログラムを読む

Java のコードは必ずの中に書き、実行は という決まった形のから始まります。実行すると Hello, sum=5 と出力されます。このコースの実測は、この Hello を class にして Hello.java に保存したものを使っています。

Java

2.class の先頭8バイトと、版が合わないとき

同じ Hello.java を 25 と JDK 22 でコンパイルし、先頭を16進で見たものです。どちらも cafe babe で始まり、違うのは版を表す番号だけでした(0x45 は 69、0x42 は 66)。JDK 25 で既定のまま作った を JDK 22 で実行すると読み込みの段階で拒否され、 に 22 を付けて作り直すと番号は 66 になり、JDK 22 でも動きました。

xxd Hello.class の先頭行と、JDK 22 での java Hello(# の行は補足)コマンド出力
# JDK 25 の javac で作った Hello.class
00000000: cafe babe 0000 0045 0032 0a00 0200 0307 .......E.2......
# JDK 22 の javac で作った Hello.class
00000000: cafe babe 0000 0042 0032 0a00 0200 0307 .......B.2......
# JDK 25 で作った Hello.class を JDK 22 の java Hello で実行
Error: LinkageError occurred while loading main class Hello
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 69.0), this version of the Java Runtime only recognizes class file versions up to 66.0

3バイトコードを javap で読む

JDK 付属の で .class の命令列を表示したものです。add は iload_0、iload_1、iadd、ireturn の4命令で、これは CPU ではなく に向けた命令です。main では 2 と 3 を積んでから add を呼んでおり、add(2, 3) は 5 に置き換えられていません。

$ javap -c -v Hello.class(JDK 25、抜粋)追跡ログ
static int add(int, int);
descriptor: (II)I
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_2
1: iconst_3
2: invokestatic #7 // Method add:(II)I
5: istore_1
6: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
9: iload_1
10: invokedynamic #19, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
15: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: return

4javac が畳むもの、畳まないもの

javac がコンパイル時に計算を済ませたのは定数式だけで、2 + 3 は iconst_5 に、"a" + "b" は ab という1つの文字列になっています。メソッド呼び出しの add(2, 3) と、何もしないループはそのまま残りました。 は受け付けられますが、出力された .class は付けない場合と1バイトも違いませんでした(JDK 25)。

$ javap -c Fold.class(JDK 25、抜粋)追跡ログ
static int viaConstant();
Code:
0: iconst_5
1: ireturn
 
static int viaCall();
Code:
0: iconst_2
1: iconst_3
2: invokestatic #7 // Method add:(II)I
5: ireturn
 
static java.lang.String s();
Code:
0: ldc #13 // String ab
2: areturn
public class Fold {
    static final int K = 2 + 3;          // 定数式
    static int add(int a, int b) { return a + b; }
    static int viaConstant() { return 2 + 3; }       // 定数式: javac が畳む
    static int viaCall()     { return add(2, 3); }   // メソッド呼び出し: 呼び出しのまま残る
    static String s()        { return "a" + "b"; }   // 定数式
    static int deadLoop()    { int x = 0; for (int i = 0; i < 10; i++) { x += 0; } return x; }
}

5Java・Rust・Node で、実行に何が要るか

同じ Hello を3つの言語で用意し、と実行に必要なものを並べた実測です。JVM の入っていない node:24 のでは Hello は command not found になり、Rust の実行ファイルはコピーするだけで動きました。Rust の大きさは での値で、rustc を直接使うと約 4.57 MB になるなど条件で変わります。

同じ Hello の成果物と実行に必要なもの(arm64 の Docker 上)表
Java Rust Node.js
ビルド javac Hello.java cargo build --release なし
成果物 Hello.class 919 B hello 469,008 B hello.js 97 B
先頭バイト CA FE BA BE 7F 45 4C 46 (ELF) (テキスト)
中身 バイトコード aarch64 の機械語 JavaScript のソース
実行に要るもの JVM OS と libc node
 
実行に要るものの大きさ(測った範囲が違うので、そのままは比べられない)
Java: libjvm.so 27,630,008 B、lib/modules 146,097,979 B(JDK 全体は du -sh で 253M)
Node: node の実行ファイル 122,893,672 B

図やコードに出てくる用語(タップで説明)