Java クラスファイル解析ツール
Java の .class ファイルはアーカイブではなく JVM 自身のフォーマットです。1 ファイルに 1 クラス、最初のバイトからビッグエンディアン、そして名前も型も定数もすべて先頭の定数プールへのインデックスとして書かれています。このツールはその構造をブラウザー内で読み取り、javap -v が報告する内容を表示します。JDK は不要で、ファイルが端末の外に出ることもありません。ヘッダーの major 番号は Java のリリースに対応します。45 は Java 1.1、49 は Java 5、52 は Java 8 で、8 以降はリリースごとにちょうど 1 ずつ増えるため 55 は 11、61 は 17、65 は 21、69 は 25 です。UnsupportedClassVersionError が指しているのはこの数字で、ファイルを読み込める最も古い JVM を示します。コンパイルに使った JDK ではありません。JDK 21 上で javac --release 8 を実行すれば major 52 のファイルが出ます。アクセスフラグを展開して、クラスかインターフェースか列挙型かアノテーションかレコードかも判定します。レコードには専用のフラグがなく Record 属性で判別し、列挙型は ACC_ENUM で判別します。定数プールをタグ別に数えるので、生成されたクラスがなぜ巨大なのかがすぐ分かります。数千個の Utf8 項目や、ラムダと文字列連結が生んだ InvokeDynamic の山がその場で見えます。フィールドとメソッドはディスクリプタを Java の型に展開して並べ、メソッドごとに Code 属性の max_stack、max_locals、コード長を添えます。抽象メソッドとネイティブメソッドには Code がないためダッシュになります。参照クラス一覧は自分自身を除くすべての CONSTANT_Class 項目で、単一のクラスファイルが持ちうる最も依存関係に近い情報です。分からないことも同じくらい重要です。ディスクリプタは消去後の型なので List<String> のフィールドは Signature 属性がなければ java.util.List としか見えず、-g:none でコンパイルしたクラスには SourceFile も行番号もローカル変数名もないためスタックトレースが空になり、実行時に名前だけで読み込まれるクラスはここには現れません。
使い方
- .class ファイルをボックスにドロップするか、クリックして選びます。コンパイル済みのクラス 1 個である必要があります。.jar は zip なので先に展開して目的のクラスを選んでください。
- 概要の行で、クラスファイルバージョン、それに対応する Java リリース、型の種別、定数プールの大きさを確認します。
- 宣言ブロックでスーパークラス、インターフェース、クラスレベルの属性、SourceFile 名を確認します。SourceFile もデバッグ情報もなければ -g:none でビルドされています。
- メソッド表を眺めます。最大スタック、最大ローカル変数、コードバイト数はすべて Code 属性から来るので、抽象・ネイティブ・インターフェースのメソッドはダッシュになります。
- コード量の割にクラスが大きすぎるときは定数プールの内訳を開いてください。タグ別の件数が原因を一行で示します。
よくある質問
- 「class file version 65.0」とは何で、UnsupportedClassVersionError はなぜ出るのですか。
- 2 つの数字はファイルの 5〜8 バイト目にある major と minor バージョンです。major 45 が Java 1.1、46、47、48 がそれぞれ 1.2、1.3、1.4 で、major 49(Java 5)以降は major から 44 を引けばリリース番号になります。つまり 52 は Java 8、55 は 11、61 は 17、65 は 21、69 は 25 です。UnsupportedClassVersionError は実行中の JVM がファイルの major バージョンより古いという意味で、メッセージには両方の数字が出ます。対処は新しいランタイムを使うか、実際に配備する版に合わせて --release を指定して再コンパイルすることです。minor 65535 は特別で、プレビュー機能を使うファイルを表し、そのリリースで --enable-preview を付けたときだけ読み込めます。
- 定数プールはなぜ long や double の次のインデックスを飛ばすのですか。
- 仕様がそう定めているからで、自作のクラスリーダーが最もよく間違える場所でもあります。CONSTANT_Long と CONSTANT_Double は連続する 2 スロットを占め、2 番目は使用できません。何もそこを指せず、次の実項目は 1 つ後のインデックスから始まります。Java の設計者も後に失敗だったと認めていますが、これまで作られたすべてのクラスファイルに焼き付いています。long の後でインデックスを 1 しか進めないリーダーは、そこから先のプール全体がずれ、フィールド名もメソッド名もクラス参照も別の項目から読んでしまいます。エラーにならず、もっともらしいのに完全に誤った一覧が出るのが厄介な点です。このツールは 2 進めるため、表示されるプール数は列挙した項目数より大きくなります。
- クラスファイル内の文字列は UTF-8 ですか。
- ほぼそうですが違いがあり、その違いが問題を起こします。CONSTANT_Utf8 は修正 UTF-8 を使います。NUL 文字は 0 バイト 1 個ではなく C0 80 の 2 バイトで符号化されるので、文字列に 0 バイトが決して含まれず、C のコードが名前をヌル終端として扱えます。基本多言語面の外の文字は、本来の UTF-8 が使う 4 バイト形式ではなく、UTF-16 のサロゲートペアに分けたうえで各サロゲートを 3 バイト形式で、合計 6 バイトで書きます。このバイト列を標準の UTF-8 デコーダーに渡すと置換文字になるか例外になります。このツールは各グループを UTF-16 コードユニットに復号してサロゲートペアを再結合させます。JVM と同じやり方です。
- List<String> のフィールドがなぜ java.util.List としか表示されないのですか。
- ディスクリプタが消去された型だからです。field_info 構造体はディスクリプタを保持しますが、ディスクリプタに型引数を書く場所はありません。Ljava/util/List; がすべてです。ジェネリクスの情報は任意属性の Signature に別途保存され、コンパイラがジェネリックなクラス・フィールド・メソッドに対してディスクリプタと並べて出力します。リフレクションや javap の宣言表示はこちらを使います。何の変哲もなさそうなクラスやメンバーの属性一覧に Signature がよく現れるのはそのためです。あるフィールドが raw な List ではなく List<String> だと分かるのは、クラスファイル内では Signature 属性だけです。
- クラス、列挙型、レコード、アノテーションはどう見分けますか。
- アクセスフラグと 1 つの属性で見分けます。ACC_INTERFACE はインターフェースを表し、アノテーション型はそれに ACC_ANNOTATION が加わったインターフェースです。javap がアノテーションを java.lang.annotation.Annotation を拡張するインターフェースとして表示するのはそのためです。ACC_ENUM は列挙型を表し、スーパークラスは java.lang.Enum、各定数は同じく ACC_ENUM の付いた static final フィールドとして、合成の $VALUES 配列とともに並びます。レコードには専用のフラグがまったくありません。java.lang.Record を拡張する final クラスで、構成要素を列挙した Record 属性を持つため、その属性だけが確実な判定材料です。ACC_MODULE は module-info ファイルを表し、フィールドもメソッドもなく、そもそもクラスではありません。
- 参照クラス一覧はこのクラスの依存関係と同じですか。
- 近いのですが、抜けを知っておく価値があります。この一覧はプール内のすべての CONSTANT_Class 項目で、スーパークラス、インターフェース、生成・キャスト・捕捉した型、フィールドやメソッドの所有者、バイトコードに現れる配列型を網羅します。しかしディスクリプタの中にしか出てこない型は含みません。String を受け取って何も返さないメソッドは (Ljava/lang/String;)V を Utf8 項目としてプールに置くだけで、java/lang/String が CONSTANT_Class として現れないこともあります。文字列で名前を指定して Class.forName で読み込むクラスや、実行時に解決されるサービスも見えません。コンパイル時の参照集合として扱い、実行時の完全な閉包とは考えないでください。
関連ツール
Python .pyc バイトコード解析
__pycache__ の .pyc をブラウザで開きます。どの CPython が書いたか、無効化方式、コードオブジェクトの木、逆アセンブルまで。
gettext .mo カタログ検査ツール
コンパイル済みの GNU gettext .mo カタログをブラウザーで読み解きます。バイト順、ヘッダー、全 msgid と訳文、コンテキスト、複数形、未翻訳項目まで表示します。
Semver ツール(パーサー・比較・バンプ)
2 つのセマンティックバージョンを並べてパース・比較・バンプ。^x.y.z や ~x.y.z が実際にカバーする範囲を展開する機能付き。
Python pickle インスペクター
.pkl ファイルをブラウザで逆アセンブル。全オペコード、メモ、復元された値、そして読み込むとコードが走るかどうかまで。
ELF バイナリ検査ツール
Linux の実行ファイル・.so・.o をブラウザーで開きます。アーキテクチャ、必要な共有ライブラリ、ビルド ID、PIE・NX・RELRO の有無まで。
Java .properties パーサー
java.util.Properties.load とまったく同じ手順で .properties を解析し、区切り文字・エスケープ・継続行の落とし穴を行単位で示します。