前編ではnewしたときのバイトコードについて追いましたので、後編ではバイトコード実行による現象=メモリの使い方やデータ構造について追っていきたいと思います。
なお私はSunやIBMのJVMしか扱ったことがないので、他の実装には当てはまらない内容が含まれるかもしれません。ご了承ください。
データはどこに作られるのか
当然メモリに作られます。もう少し厳密に言うと、JavaHeapに作られます。
そもそもJVMはデータをどのように扱うのか?
JVM仕様書のRuntime Data Areasの節に、データを扱う6つの領域についての説明があります。
- Program Counter Register
- JVM stacks
- Heap (Java Heap)
- Method Area
- Runtime Constant Pool
- Native Method Stacks
これらの領域のうち、newされたオブジェクトの情報が記録されるのがHeapです。ネイティブなHeapと区別するためにJavaHeapとも呼ばれます。なお世代別GCをサポートしているJVMなら、JavaHeapの中のEDEN領域と呼ばれる領域に作られます*1。
ただしJava SE 5以降のSun JVMでは、マルチスレッド環境下におけるHeapの取り合いを回避するために、スレッドごとに用意された領域に作られるケースもあるようです。
For multithreaded applications, allocation operations need to be multithread-safe. If global locks were used to ensure this, then allocation into a generation would become a bottleneck and degrade performance. Instead, the HotSpot JVM has adopted a technique called Thread-Local Allocation Buffers (TLABs).
Memory Management in the Java HotSpot〓 Virtual Machine
さらにJava SE 6からはエスケープ解析によってスタックに作ることも可能だそうです。ので基本的にはJavaHeapのEDEN領域に作られるが、高速化のためにより被アクセスが限定された領域に作られることもあると認識しておけば良いのではないでしょうか。
世代別GCにおけるメモリ管理の詳細は、@ITの記事などを参照してください。
- Tuning Garbage Collection with the 1.4.2 Java Virtual Machine
- チューニングのためのJava VM講座(後編):ガベージコレクタの仕組みを理解する (1/2) - @IT
以前流行ったGC本もなかなか良いと思います。
どのようなデータが作られるか・Sun JVMの場合
情報が見つかっていません。
以前「各オブジェクトごとにヘッダが8バイト存在する」という記述を見かけた覚えがあるのですが……見つかり次第追記します。
どのようなデータが作られるか・IBM JVMの場合
IBMのJVMに関してはdeveloperWorksに記事があります。
各オブジェクトに共通な情報を表現するためのヘッダが割り当てられ、そのヘッダに続いて各オブジェクト固有のオブジェクトデータが記録されます。
ヘッダ
ヘッダは3つの領域に分かれています。これらはすべてのオブジェクトに共通です。
つまりフィールドを一切持たないオブジェクトでも、32bitアーキテクチャなら最低16バイト*2、64bitアーキテクチャなら最低24バイトを消費する計算になります。
- size+flags
- mptr
- locknflags
オブジェクトデータ
オブジェクトデータ(Object data)のレイアウトはオブジェクトに依存すると記載されていますが、おそらくはフィールド値を順番にならべたシンプルな実装になっているものと思われます。
オブジェクトのデータサイズについて
データサイズがどのくらいになるか?はやはりJVMのベンダに依存しますが、比較的簡単なコードで簡単な検証が可能です。
私のMac OSXデフォルトのJava1.6.0(64bit)では、以下のような結果が得られました。
フィールドの合計サイズが多いほどデータサイズが大きくなるという、直感に従った結果となっています。フィールドの数には直接依存しないので、byteはきちんと1バイトで表現している(intとして扱ったりしてはいない)のでしょう。
java.lang.Object, 0 fields, 24 bytes Test$HasOneField, 1 fields, 24 bytes Test$HasTwoField, 2 fields, 24 bytes Test$HasThreeField, 3 fields, 32 bytes Test$HasLongField, 1 fields, 24 bytes Test$HasTwoLongField, 2 fields, 32 bytes Test$HasIntAndLongField, 2 fields, 32 bytes Test$HasObjectField, 1 fields, 24 bytes Test$HasTwoObjectField, 2 fields, 32 bytes Test$HasEightByteField, 8 fields, 24 bytes Test$HasNineByteField, 9 fields, 32 bytes java.util.ArrayList, 3 fields,144 bytes
注意すべきなのは、オブジェクトデータの保存は連続領域を要求するということです。例えば64bitアーキテクチャにおいて、new int[32]は(24+4*32)バイトの連続領域を要求します。
連続領域がJavaHeapから得られない場合はGCが走ります*3ので、フィールド数の大きなクラスや要素数の大きな配列のインスタンス化は比較的高いコストがかかります。
また参照値は32bitアーキテクチャで4バイト、64bitアーキテクチャで8バイトになることが予想されます。同じJavaバイトコードでも、実行するJVMのアーキテクチャによってメモリ要求量が2倍に増えてしまうわけです。LinkedListのように参照をフィールドに持つインスタンスを大量生成するクラスでは、この差が顕著に現れるでしょう。(追記:参照を圧縮するオプションもあります)
まとめ
前後編にわけて「Javaでnewしたら何が起こるか」を追ってきました。まとめると以下のようになるでしょう。
- メモリの確保(バイトコードnew)とコンストラクタ呼び出し(バイトコードinvokespecial)にの2段階に分けて実施
- データは基本的にJavaHeapのEDEN領域に確保される
- データのレイアウトと大きさはJVM依存
オペランドスタックやGCなど、JVMの実装を垣間見るきっかけとして大変面白い問いかけであると思いますので、興味を感じた方はぜひSunやIBMの資料を紐解いてみてください。
- JVM仕様書
- 本連載で参考にした世代別GC関連資料
- http://www.atmarkit.co.jp/fjava/rensai3/javavm02/javavm02_1.html
- http://www.ibm.com/developerworks/library/i-garbage1/
- http://www.ibm.com/developerworks/ibm/library/i-garbage2/
- http://java.sun.com/docs/hotspot/gc1.4.2/index.html#3.%20Sizing%20the%20Generations%7Coutline
- http://java.sun.com/j2se/reference/whitepapers/memorymanagement_whitepaper.pdf