Java class ファイル形式
コンパイル済みの Java バイトコード。JVM が実行します。
- MIME タイプ
- application/java-vm
- 拡張子
- .class
- カテゴリ
- 実行ファイル・バイトコード
マジックナンバー(ファイル署名)
| 0x00000000 | CAFEBABE | ···· | 'CAFEBABE' magic |
Java class ファイル形式について
.class ファイルはコンパイル済みの Java の型が 1 つ(クラス、インターフェース、enum、record のいずれか)JVM 自身のコンテナ形式に収まったもので、その中身のほとんどは 1 つの構造、すなわち定数プールです。クラス名、メソッド名、フィールドのディスクリプタ、文字列リテラル、あらゆるシンボリック参照がここに番号付きのエントリとして入り、ファイルの残りはそれらをテキストではなくインデックスで参照します。class ファイルが JAR の中でよく縮むのも、クラス名を変えるとファイル中のあちこちのバイトが変わるのも、定数プールより後ろのオフセットがすべて可変なのもこのためです。プールの長さは、その型が言及する名前の数だけで決まります。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 4 | magic | CA FE BA BE。Java にまだ名前が無かった頃に James Gosling が選んだもので、間の悪いことに Apple のユニバーサルバイナリと共有しています。ファイル中で値が固定されている唯一のフィールドです。 |
| 4 | 2 | minor_version | ほぼ常に 0。値 65535 には特別な意味があり、そのクラスがプレビュー機能を使っていて、コンパイルしたのとまったく同じ JDK でしかロードできないことを示します。メジャーバージョンが合っているのにプレビュー機能のクラスが読み込めない、という現象の正体はこれです。 |
| 6 | 2 | major_version | class ファイルのバージョンで、あらゆる UnsupportedClassVersionError の背後にあるフィールドです。Java 5 以降はフィーチャーリリース番号 + 44 で、Java 8 が 52、Java 17 が 61、Java 21 が 65。ランタイムが他の何かを読む前に、自分の上限と比べるのがこの 2 バイトです。 |
| 8 | 2 | constant_pool_count | プールのエントリ数より 1 大きい値です。プールが 1 始まりで、インデックス 0 は予約されているからです。小さなユーティリティクラスなら数十、大きなフレームワークのクラスは上限の 65535 に近づくこともあり、生成コードでは実際にぶつかることのある制限です。 |
| 10 | variable | constant_pool | エントリが隙間なく並び、それぞれ長さを決める 1 バイトのタグで始まります。タグ 1 は 2 バイトの長さ接頭辞を持つ UTF-8 文字列、タグ 7 はクラス参照、タグ 10 はメソッド参照、タグ 8 は文字列。タグ 5 と 6(long と double)はインデックスの枠を 2 つ消費します。この形式の中で、手書きのパーサーを一番多く壊してきた規則です。 |
| variable | 2 | access_flags | プールの直後の最初のフィールドです。0x0001 が public、0x0010 が final、0x0200 が interface、0x4000 が enum。そして 0x0020(ACC_SUPER)は、旧来の invokespecial の意味論のために 1997 年以降にコンパイルされたほぼすべてに立っています。素の public クラスなら通常 0x0021 と読めます。 |
| variable | 2 | this_class | この型の名前を持つプールエントリのインデックスで、ドットではなくスラッシュを使う内部形式です。JAR の中でのファイルのパスと一致していなければならず、ディスク上で .class ファイルをリネームすると、両方の綴りを並べた NoClassDefFoundError が出るのはそのためです。 |
| variable | 2 | super_class | スーパークラスのエントリのインデックスで、通常は java/lang/Object。0 になるのは java/lang/Object 自身だけで、これは世界で唯一、親を持たないクラスです。 |
| variable | 2 | interfaces_count | この後ろに続く直接のスーパーインターフェースの数で、それぞれ 2 バイトのプールインデックスです。そこからフィールド、メソッド、クラス属性が続き、どれも長さ接頭辞付きなので、プール以降のファイルは順番に読むしかありません。目次にあたるものはありません。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| Mach-O(ユニバーサル) | どちらも CA FE BA BE で始まります。4〜7 バイト目を読んでください。class ファイルは minor_version、major_version の順で、major_version は最低 45(Java 1.1)、今なら 60 台から 70 台です。ユニバーサルバイナリはそこに nfat_arch、つまり小さなアーキテクチャ数が入るので上位半分はゼロ。6〜7 バイト目が 44 より大きい非ゼロなら class ファイルです。 |
| Java JAR | JAR はオフセット 0 に class ファイルを持ちません。50 4B 03 04 で始まる ZIP で、圧縮された class ファイルを中に抱えています。JAR のバイト列に CA FE BA BE が現れるのは想定どおりで、ただ先頭には無いというだけ。アーカイブの正体を示すのはセントラルディレクトリのエントリ名です。 |
| Android DEX | Android のツールは .class ファイルをコンパイルしてから捨てます。d8 がそれらをまとめて DEX ファイルにするのです。DEX は 'dex' と改行で始まり、JVM のスタックマシンではなくレジスタベースの命令セットを使います。だから APK には .class のエントリが 1 つも入っておらず、Android 向けの JAR と APK を見分ける一番手早い方法がこれです。 |
| Java source (.java) | ビルドディレクトリで混ざるので書いておきます。.java ファイルはプレーンテキストですが、.class ファイルは UTF-8 テキストとして妥当でないバイトで始まります。.class を開いてクラス名やメソッド名を読める形で表示するツールは、ソースではなく定数プールの UTF-8 エントリを読んでいます。 |
よくある壊れ方
- ランタイムより新しい major_version。破損ではまったくないのですが、人が眺めているのはたいていこれです。JVM は 6〜7 バイト目を読み、自分の上限より上のバージョンを見つけると、他を一切パースせずに UnsupportedClassVersionError を投げます。release 21 でコンパイルして Java 17 で動かすと、まさにこうなります。
- 途中切れ。定数プールより後ろはすべて長さ接頭辞付きで順番に並んでいるので、切られた class ファイルはそのとき読んでいた構造の場所で ClassFormatError になり、メッセージが本当の原因を指すことはめったにありません。メッセージを読むより、ファイルが短いことに気づくほうが早いです。
- 定数プールの中のバイトを編集した。文字列リテラルを別の長さのものに変えて 2 バイトの長さ接頭辞を直さないと、以降のエントリが全部同期を失い、パーサーは名前の途中からタグを読んで「未知の定数タグ」と報告します。class ファイルへのパッチに hex エディタではなくライブラリが要るのはこのためです。
- テキストモード転送。古い FTP の問題は、レガシーなビルドパイプラインの中で今も class ファイルに届きます。CR LF 変換は最初の出現より後ろを全部ずらし、定数プールが長さ接頭辞付きエントリの連なりであるために、失敗はマジックナンバー不一致のような綺麗な形ではなく、意味不明なタグとして現れます。
この形式の現在地
class ファイルは JVM の実際のインターフェースです。Java も Kotlin も Scala も Groovy も Clojure もこれにコンパイルされ、だからこそ 5 つではなく 1 つの生態系になっています。2005 年にコンパイルされたライブラリが今日のコードとリンクできるのも、この形式が安定しているからです。単体で扱うことは稀で、クラスは JAR や WAR、モジュールイメージ、コンテナのレイヤーの中を移動します。それでも、バイトコードツールも難読化ツールもカバレッジエージェントも、実行時にプロキシを生成するフレームワークも、実際に触っているのはこれです。目立った例外が Android で、ビルド時に class ファイルを取り込み、出荷するのは DEX です。hex エディタを持つ人にとって有用な性質は、class ファイルがかなりの部分そのまま読めるテキストだということ。定数プールはその型が触れるクラス・メソッド・フィールドをすべて綴っているので、逆コンパイルしなくても依存関係が読めます。
Java class ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
Java class ファイルと混同しやすい形式です。先頭バイトを見比べてください。
| 形式 | 先頭バイト | 拡張子 |
|---|---|---|
| Java JAR | 50 4B 03 04 | .jar |
| Mach-O(ユニバーサル) | CA FE BA BE | .dylib |
| Android DEX | 64 65 78 0A | .dex |