1 public class AnimalHandler {2 public static void main(String[] args) {3 AnimalHandler instance = new AnimalHandler();4 }5 }
JVMでの実行時処理を理解するため、このクラスの字節コードを分析します。javap -c AnimalHandler コマンドで取得可能な内容をもとに、オブジェクト生成の内部プロセスを詳細に解説します。
1 Compiled from "AnimalHandler.java" 2 public class AnimalHandler { 3 public AnimalHandler(); 4 Code: 5 0: aload_0 6 1: invokespecial #1 // 親クラス Object のコンストラクタ呼び出し 7 4: return 8 9 public static void main(java.lang.String[]);10 Code:11 0: new #2 // 新しいインスタンスのメモリ割り当て(未初期化状態)12 3: dup // オペランドスタック上の参照を複製13 4: invokespecial #1 // コンストラクタ <init>()V を実行14 7: astore_1 // 局所変数表のインデックス1に参照を格納(instance)15 8: return16 }</init>
1. 字節コードの逐次解説
-
aload_0局所変数テーブルのインデックス0(this)をオペランドスタックに読み込む。これは現在のインスタンスへの参照であり、コンストラクタ内で使用される。 -
invokespecial #1常量プールのインデックス1にあるメソッドを呼び出す。ここではjava/lang/Object.<init>()Vが対象で、親クラスのデフォルトコンストラクタを実行する。この命令は、スタックトップの参照(this)を取り除き、コンストラクタの実行を開始する。 -
return(構造体用) メソッドの終了を示し、スタックをクリアして制御を戻す。戻り値がないため、単なる制御転送である。
-
new #2クラス情報(AnimalHandler)をもとに、ヒープ上に新しいインスタンスのメモリ領域を確保する。ただし、初期化は行われず、返されるのは「未初期化オブジェクト」の参照。この参照はオペランドスタックにプッシュされる。 -
dupオペランドスタックのトップにある参照をコピーし、再度スタックに積む。この操作は、後にコンストラクタ呼び出しと局所変数への保存の両方で同じ参照を使用できるようにするため必須。 -
invokespecial #1インスタンスの初期化処理を実行する。本クラスの<init>()メソッドが呼び出され、内部的にはsuper()により親クラスの初期化が行われる。このとき、スタックから一つの参照が消費されるが、dupによってもう一つ残っているため、後続処理で利用可能。 -
astore_1オペランドスタックから参照を取り出し、mainメソッドの局所変数テーブルのインデックス1に保存する。これにより、instance変数が新しく生成されたインスタンスを指すようになる。 -
return(main用)mainメソッドの終了を示し、スタックフレームを破棄してプログラムを終了させる。
2. JVMのメモリ構造とデータフロー
-
メソッドエリア(Method Area) クラスのメタデータ、定数プール、静的変数などを格納。
#1はObject.<init>()V、#2はAnimalHandlerのクラス参照を表す。 -
ヒープ(Heap) すべてのインスタンスが確保される領域。
new指令によって割り当てられる。マルチスレッド環境下でも共有される。 -
スタック(Stack) 各スレッドごとに独立したスタックを持つ。各関数呼び出しに対してスタックフレームが作成され、その中に局所変数テーブルとオペランドスタックが含まれる。
3. オブジェクト生成のステップバイステップ
ステップ1: new #2 実行
- ヒープ上に
AnimalHandlerインスタンスのメモリ領域を確保。 - 未初期化状態のオブジェクトが生成され、その参照がオペランドスタックにプッシュされる。
- 内容例:
ヒープ: [AnimalHandler(未初期化)]
スタック: [ref]
メソッドエリア: [AnimalHandler, #2 → AnimalHandler]
ステップ2: dup 実行
- スタックトップの参照を複製し、二重の参照が存在する状態に。
- コンストラクタ呼び出しと後続の代入に共通の参照が必要なため不可欠。
- 内容例:
スタック: [ref, ref]
ステップ3: invokespecial #1 実行
- 一方の参照が消費され、
<init>()メソッドが実行される。 thisとして渡された参照を使って、フィールドの初期化や親クラスコンストラクタの呼び出しが行われる。- 初期化完了後、スタックにはもう一つの参照が残る。
- 内容例:
ヒープ: [AnimalHandler(初期化済み)]
スタック: [ref] ← 初期化済みオブジェクトの参照
ステップ4: astore_1 実行
- スタックからの参照を局所変数テーブルのインデックス1に格納。
- 最終的に
instance変数がインスタンスを指すようになる。 - 内容例:
スタック: []
局所変数: [0: args, 1: instance → ref]
4. dup の役割と new がコンストラクタを呼ばない理由
-
なぜ
dupが必要か?invokespecialはスタックから参照を消費するため、一度だけしか利用できない。しかし、その後astoreで参照を保存する必要がある。そのため、事前にスタックにコピーしておく必要がある。この設計により、オブジェクト生成と初期化の処理が明確に分離されている。 -
なぜ
newが初期化を行わないのか?newは「メモリ確保」と「参照の生成」のみを行う。初期化は明示的なコンストラクタ呼び出し(invokespecial)によって実行される。この分離により、以下のような柔軟性が得られる: -
オブジェクトの初期化が失敗した場合のエラーハンドリングが容易。
-
パラメータ付きの初期化や、シリアライズからの復元など、特別な初期化パターンに対応可能。
-
インスタンス生成と初期化のタイミングを明確に区別できる。
この設計により、Javaの new AnimalHandler() は、正確に以下の字節コードシーケンスに対応している:
- ヒープにメモリを割り当て(
new) - 参照を複製(
dup) - 初期化処理を実行(
invokespecial) - 局所変数に保存(
astore)
すべての段階で、ヒープとスタック間の参照の流れが明確に管理されている。