私的まとめ。2段階に分かれており、今回はその初回。
JVM内の挙動を知るにはJVMに渡されるバイトコードを知ることからと考え、これについてまとめる。
バイトコードの確認
例えば
public class Test { public static void main(String[] args) { Test test = new Test(); } }
を
$ javap -c Test
すると、mainメソッドのバイトコードが
public static void main(java.lang.String[]); Code: 0: new #1; //class Test 3: dup 4: invokespecial #16; //Method "":()V 7: astore_1 8: return
になっていることがわかる。コンストラクタ呼び出しがnew,dup,invokespecialという3つのオペコードによって実現されていることがわかる。
これらのバイトコードは何をやっているのか
newからastore_1まで順を追って説明すると:
- newでインスタンスを生成して参照をスタックに積む
- JVMSに"Allocate uninitialized space"と記載があるように、この時点でメモリはまだ初期化されていない
- dupでスタックから値を取り出し、複製して再度スタックに積む(インスタンスへの参照が2つ積まれた状態になる)
- invokespecialでスタック最上部の参照が指すインスタンスの<init>メソッドを実行
- <init>メソッド=コンストラクタの別名と考えて良い
- JVMSでは"every constructor (§2.12) appears as an instance initialization method that has the special name <init>."と説明されている
- astore_1でスタックに残っている参照をローカル変数に積み替える
注目したいのは以下の2点。
- 容量の確保がコンストラクタ実行とは別に行われていること
- 容量を確保した時点でインスタンスの参照が存在するということ
これらから"コンストラクタ実行が完了していないインスタンスが誰かから参照される"可能性に気づく。実際、JDK1.4より前のJVMではメモリ操作のリオーダにより問題が生じるケースが有名だったらしい。
現在のJDKではfinalizeメソッドを使うことでコンストラクタが未完了なインスタンスに対する参照を捕まえることができるが、上記で確認したバイトコードの動作とは無関係。
public class Test { public static void main(String[] args) { try { new Inner(); } catch (OutOfMemoryError e) { System.gc(); System.runFinalization(); } } static class Inner { private final String a = new String("a"); private final String b = new String(new byte[Integer.MAX_VALUE]); private final String c = new String("c"); public void finalize() { System.out.printf("%s,%s,%s%n", a, b, c); // => a, null, null } } }
次回の予定
次回はもう少し踏み込んでメモリ管理の話を調べてまとめたい。Escape Analysisも原理や利点以外はよくわかっていないので調べたいところ。
できれば参照を管理するテーブルについても調べたいが、非公式ページにしかその記述を見つけられていないので難しいかもしれない。