Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

newするとJVM内で何が起こるのかという話(後編)

前編ではnewしたときのバイトコードについて追いましたので、後編ではバイトコード実行による現象=メモリの使い方やデータ構造について追っていきたいと思います。
なお私はSunやIBMJVMしか扱ったことがないので、他の実装には当てはまらない内容が含まれるかもしれません。ご了承ください。

データはどこに作られるのか

当然メモリに作られます。もう少し厳密に言うと、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の記事などを参照してください。

以前流行ったGC本もなかなか良いと思います。

どのようなデータが作られるか

JavaHeapにはどのようなデータが乗るのでしょうか?私の知る限りでは、JVM仕様書にはこの定義が記載されていません。ベンダによって決められるようです。

どのようなデータが作られるか・Sun JVMの場合

情報が見つかっていません。
以前「各オブジェクトごとにヘッダが8バイト存在する」という記述を見かけた覚えがあるのですが……見つかり次第追記します。

どのようなデータが作られるか・IBM JVMの場合

IBMJVMに関してはdeveloperWorksに記事があります。
各オブジェクトに共通な情報を表現するためのヘッダが割り当てられ、そのヘッダに続いて各オブジェクト固有のオブジェクトデータが記録されます。

ヘッダ

ヘッダは3つの領域に分かれています。これらはすべてのオブジェクトに共通です。
つまりフィールドを一切持たないオブジェクトでも、32bitアーキテクチャなら最低16バイト*2、64bitアーキテクチャなら最低24バイトを消費する計算になります。

オブジェクトデータ

オブジェクトデータ(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依存
    • IBMJVMでは、データは各オブジェクト共通の情報を表現するヘッダとオブジェクト固有の情報を表現するオブジェクトデータに分かれる

オペランドスタックやGCなど、JVMの実装を垣間見るきっかけとして大変面白い問いかけであると思いますので、興味を感じた方はぜひSunやIBMの資料を紐解いてみてください。

*1:例外としてEDEN領域に入りきらないオブジェクトは直接OLD領域に作られたりする

*2:4バイト*3=12バイトだがヒープは8バイトずつ割り当てられるため最低16バイトとなる

*3:IBM JVMにおける挙動についてはこちらに記載がありますが、他のJVMでも同様でしょう