
はじめに
こんにちは、TIGの岸本卓也です。 春の入門祭り2025 シリーズです。
私は Java の経験が長いのですが、JVM やバイトコードといった Java で処理が実行される根本的な仕組みへの理解が薄かったため改めて向き合うことにしました。本稿では理解の足がかりとなる調査方法を提示します。
環境
この検証は JDK 24 を使用し Windows 環境で実施しました。JDK の詳細なバージョンは次のとおりです。
openjdk version "24" 2025-03-18 |
“Hello World!” サンプルソース
シンプルな処理の例として、 Java Tutorials で提示されている以下の “Hello World!” ソースを題材にします。
/** |
なお JDK 21 以降では、main メソッドを以下のようにクラス定義無しで実装可能な機能 (JEP 445: Unnamed Classes and Instance Main Methods (Preview)) がプレビューで導入されています。
void main() { |
これはまだプレビューかつ入門用の側面が強い機能のため、本稿ではこれまで通りクラス定義のある “Hello World!” ソースで試します。
コンパイル
Java ソースを実行するにはコンパイルが必要です。Java コンパイラ (javac
コマンド) ではネイティブアプリにコンパイルするのではなく Java 仮想マシン (JVM) 用のバイナリにコンパイルします。JVM 用のバイナリはバイトコードと呼ばれ、たいていは拡張子が .class
のクラスファイル形式で出力します。JVM は以下のように OS 上で動作しコンピューターの CPU アーキテクチャーや OS の違いを吸収してバイトコードを実行する役割を担っています。
block-beta |
サンプルソースを次のようにコンパイルすると HelloWorldApp.class
が生成されます。
javac HelloWorldApp.java |
クラスファイルはバイナリファイルでありそのままでは読み解くことが困難ですが、JDK に同梱の javap
コマンド で逆アセンブルしてバイトコードを分かりやすく表示させることができます。以下のように -v
オプションを付けて実行すると、クラスファイルの詳細が表示されます。
javap -v HelloWorldApp.class |
今回の場合は以下のように表示されました (一部マスク処理済み)。
Classfile /C:/***/HelloWorldApp.class |
Code ブロックには JVM の命令セット に基づく処理が並んでいることが分かります。
サンプルソースと対応付けやすい部分として main メソッドに注目して Java ソースとバイトコードの該当箇所を抜粋したものは以下です。
public static void main(String[] args) { |
public static void main(java.lang.String[]); |
JVM ではメソッド起動時に新たな フレーム を作成して各種データを保持します。フレームにはオペランドスタックがあり、JVM 命令はこのオペランドスタックを使って処理を実行します。上記 main メソッドの Code ブロックの例では以下の処理になります。
- 命令インデックス0 (
getstatic #7
): 定数プール #7の値 (System.out
フィールドの参照) をスタックにプッシュする - 命令インデックス3 (
ldc #13
): 定数プール #13の値 (String
型の"Hello World!"
) をスタックにプッシュする - 命令インデックス5 (
invokevirtual #15
): 定数プール #15 (java.io.PrintStream#println(String)
メソッドの参照) が示すメソッドを起動する。今回の場合、命令インデックス3でプッシュした値をスタックからポップし、新たなフレームを作成してそのフレームでメソッドの命令を実行していく。 - 命令インデックス8 (
return
): 呼び出し元にvoid
を返却する
プログラムの実行
コンパイルしたバイトコードは java
コマンド を使って以下のように実行できます。
java -cp . HelloWorldApp |
この実行ではクラスパス -cp .
とメインクラス HelloWorldApp
を指定しています。JVM はクラスパスに従ってカレントディレクトリからメインクラスを検索してロードし、メインクラスに定義された main
メソッドを実行します。
プログラムの起動から終了までの流れは Java 言語仕様の Execution によると以下のように行われます。
- JVM の起動
- クラスのロード
- リンク: クラスの検証、static フィールドやメソッドテーブルの準備、シンボル参照の解決
- クラスの初期化: イニシャライザーの実行
<メインクラス>.main
メソッドの実行
- プログラムの終了: 以下のいずれかが発生すると終了する。
- すべての非デーモンスレッドとシャットダウンフックが終了した。
System.exit
,Runtime.exit
,Runtime.halt
のいずれかが呼び出された。- JVM が外部から終了リクエストを受け取った (例えばシグナル)。
- JVM でハンドリングできない外部イベントが発生した (例えばプロセスの異常やコンピューターの電源断)。
これらの処理が具体的にどのように起きているかは -Xlog
オプション で出力できる JVM ログが参考になります。例えば、すべてのログを vm_%t_%p.log
ファイルに出力する実行例は以下です。
java -Xlog:all=trace:vm_%t_%p.log:uptime,level,tid,tags -XX:LogClassLoadingCauseFor=* -cp . HelloWorldApp |
また、 -Xlog
オプションとは関係がありませんが、 _JAVA_LAUNCHER_DEBUG=1
といった環境変数を設定すると JVM 起動に関する追加の情報がコンソールに出力されます。
JVM ログを概観した結果、JVM 起動の各処理は以下のタグまたはタグセットのログが参考になりそうです。
- クラスのロード:
class+load*
- クラスの検証:
verification
- シンボル参照の解決:
class+resolve
- クラスの初期化:
class+init
さいごに
本稿では Java で “Hello World!” の処理が実行される仕組みをバイトコードや JVM を交えて説明し、調査に使える機能を紹介しました。調査にあたっては以下のページを参考にしました。