[Javaベース]Javaオブジェクトメモリ構造

10275 ワード

転載先:http://www.importnew.com/1305.html
原文は2008年11月13日に発表され、2008年12月18日更新:ここ JavaのSizeof演算子のユーティリティライブラリに関する記事もあります.
C/C++出身の私は、Javaに少し戸惑っています.それは、計算対象がメモリサイズを占有するメカニズムが欠けていることです.一方,C++ではsizeof演算子により基本タイプとクラスインスタンスのサイズを得ることができる.CとC++のこのオペレータは、ポインタ演算、メモリコピー、IO操作に非常に役立ちます.
Javaには似たような演算子はありません.実際、Javaにもこのような演算子は必要ありません.Javaの基本タイプのサイズは言語仕様で定義されていますが、C/C++の基本タイプのサイズはプラットフォームに関連しています.Javaには独自のシーケンス化によって構築されたIOフレームワークがある.なお,Javaにはポインタがないため,ポインタ演算やメモリブロックコピーなどの操作も存在しない.
しかし、Javaプログラマーは、Javaオブジェクトがどれだけのメモリを使用しているかを知りたい場合があります.しかし、この問題の答えは簡単ではありません.
まずはっきり区別するのはshallow sizeとdeep sizeです.Shallow size(Shallow size)とは、オブジェクト自体が消費するメモリサイズで、参照オブジェクトのサイズは含まれません.deep sizeは、自身が占めるメモリサイズと、その再帰的に参照されるすべてのオブジェクトが占めるメモリサイズの合計です.多くの場合、オブジェクトのdeep sizeを取得したいと思っていますが、この値を知るためには、まずshallow sizeの計算方法を知っておく必要があります.次に紹介します.
JVM仕様には、実行時のJavaオブジェクトのメモリ構造の説明がないという苦情があります.これは、JVMベンダーが自分のニーズに合わせて実現できるということです.結果として、同じクラスで異なるJVM上で実行されるインスタンスオブジェクトのメモリサイズが異なります.幸いなことに、世界のほとんどの人(私を含む)がSun HotSpot仮想マシンを使用しているので、この問題を大幅に簡略化しました.我々の次の議論も32ビットのSun社のJVMに基づいている.次に、JVMがメモリ内のオブジェクトのレイアウトをどのように組織するかを説明するルールについて説明します.
インスタンス属性のないクラスのメモリレイアウト
Sun JVMでは,オブジェクトに2つのワード(words)のヘッダがある.第1のワードには、このオブジェクトのタグハッシュコードおよび他のロック状態および等の識別情報が含まれ、第2のワードには、オブジェクトを指すクラスへの参照が含まれます.また、どのオブジェクトも8バイトで粒度を揃えます.これがオブジェクトメモリレイアウトの最初のルールです.
ルール1:どのオブジェクトも8バイトで粒度を揃えます.
たとえば、new Object()を呼び出すと、Objectクラスに他の格納可能なメンバーがいないため、スタック内の8バイトのみを使用して2文字のヘッダを保存できます.
Objectを継承したクラスのメモリレイアウト
上記の8バイトのヘッダを除いて、クラス属性は後続します.プロパティは通常、そのサイズに基づいて並べ替えられます.例えば、整数(int)は4バイト単位で整列し、長整数(long)は8バイト単位で整列する.ここでは、通常、データが4バイト単位で整列されている場合、メモリから4バイトのデータを読み出し、プロセッサに書き込まれる4バイトレジスタは、より高い価格で設計されている.
メモリを節約するために、Sun VMはプロパティ宣言時の順序でメモリレイアウトを行っていません.実際には、プロパティはメモリ内で次の順序で整理されます.
1.二重精度型(doubles)とロング整数型(longs)
2.整数型(ints)と浮動小数点型(floats)
3.ショートタイプ(shorts)と文字タイプ(chars)
4.ブール型(booleans)とバイト型(bytes)
5.参照タイプ(references)
メモリ使用率はこのメカニズムによって最適化されます.たとえば、次のようにクラスを宣言します.
class MyClass {
 
       byte a;
 
       int c;
 
       boolean d;
 
       long e;
 
       Object f;         
 
}

JVMがプロパティの宣言順序を乱さない場合、オブジェクトのメモリレイアウトは次のようになります.
[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       4 bytes] 16
[d:       1 byte ] 17
[padding: 7 bytes] 24
[e:       8 bytes] 32
[f:       4 bytes] 36
[padding: 4 bytes] 40

この場合、占有に使用される14バイトは無駄であり、このオブジェクトには40バイトのメモリ領域が使用される.ただし、上記のルールでオブジェクトを並べ替えると、メモリの結果は次のようになります.
[HEADER:  8 bytes]  8
[e:       8 bytes] 16
[c:       4 bytes] 20
[a:       1 byte ] 21
[d:       1 byte ] 22
[padding: 2 bytes] 24
[f:       4 bytes] 28
[padding: 4 bytes] 32

今回、占有に使用されるのは6バイトのみで、このオブジェクトは32バイトのメモリスペースを使用しています.
したがって、オブジェクトメモリレイアウトの2番目のルールは次のとおりです.
ルール2:クラス属性は、長整型と二重精度型の優先順位で並べられます.整数型と浮動小数点型;文字と短い整数;バイトタイプとブールタイプ、最後にリファレンスタイプです.これらのアトリビュートは、それぞれの単位で位置合わせされます.
次に、Objectを継承したクラスのインスタンスのメモリサイズを計算する方法について説明します.次の例は、java.lang.Booleanの練習に使用します.これはメモリレイアウトです.
[HEADER:  8 bytes]  8
[value:   1 byte ]  9
[padding: 7 bytes] 16

Booleanクラスのインスタンスは16バイトのメモリを消費します!驚いたでしょう?(最後に占有に使用した7バイトを忘れないでください).
他のクラスのサブクラスのメモリレイアウトを継承
JVMが遵守する次の3つのルールは、親クラスのクラスのメンバーを整理するために使用されます.オブジェクトメモリレイアウトのルール3は次のとおりです.
ルール3:異なるクラスの継承関係のメンバーを混在させることはできません.まず、ルール2に従って親クラスのメンバーを処理し、次に子クラスのメンバーを処理します.
例を次に示します.
class A {
   long a;
   int b;
   int c;
}
 
class B extends A {
   long d;
}

クラスBのインスタンスは、メモリに以下のように格納される.
[HEADER:  8 bytes]  8
[a:       8 bytes] 16
[b:       4 bytes] 20
[c:       4 bytes] 24
[d:       8 bytes] 32

親クラスのメンバーのサイズが4バイトという基本単位を満たすことができない場合は、次のルールが役立ちます.
ルール4:親の最後のメンバーと子の最初のメンバーの間隔が4バイト未満の場合、4バイトの基本単位に拡張する必要があります.
例を次に示します.
class A {
   byte a;
}
 
class B {
   byte b;
}
[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[b:       1 byte ] 13
[padding: 3 bytes] 16

メンバーaは、メンバーbとの間隔が4バイトであることを保証するために3バイト拡張されることに留意されたい.この空間はクラスBには使えないので無駄にされている.
最後のルールは、サブクラスメンバーがロングまたはデュアル精度タイプで、親クラスが8バイトも使い果たさない場合、スペースを節約するために使用されます.
ルール5:サブクラスの最初のメンバーが二重精度または長整数であり、親クラスが8バイトを使い切っていない場合、JVMはルール2を破壊し、整形(int)、短整数(short)、バイト型(byte)、参照タイプ(reference)の順に、満たされていない空間に埋め込まれます.
例を次に示します.
class A {
  byte a;
}
 
class B {
  long b;
  short c; 
  byte d;
}

メモリレイアウトは次のとおりです.
[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       2 bytes] 14
[d:       1 byte ] 15
[padding: 1 byte ] 16
[b:       8 bytes] 24

12バイト目でクラスAが「終了」した場合、JVMはルール2を守らず、長い整数の前に短い整数と1バイトのメンバーを挿入することで、3バイトの無駄を避けることができます.
配列のメモリレイアウト
配列には、長さ変数を格納する追加のヘッダメンバーがあります.配列要素および配列自体は、他の通常のオブジェクトと同様に8バイトの境界規則を遵守する必要があります.
次に、3つの要素を持つバイト配列のメモリレイアウトを示します.
[HEADER:  12 bytes] 12
[[0]:      1 byte ] 13
[[1]:      1 byte ] 14
[[2]:      1 byte ] 15
[padding:  1 byte ] 16

次に、3つの要素を持つロング整数のメモリレイアウトを示します.
[HEADER:  12 bytes] 12
[padding:  4 bytes] 16
[[0]:      8 bytes] 24
[[1]:      8 bytes] 32
[[2]:      8 bytes] 40

内部クラスのメモリレイアウト
非静的内部クラス(Non-static inner classes)には、外部クラスへの参照変数である追加の「非表示」メンバーがあります.このメンバーは通常のリファレンスであるため、リファレンスメモリレイアウトのルールを遵守します.したがって、内部クラスには4バイトの追加オーバーヘッドがあります.
最後の考え
Javaオブジェクトのshallow sizeを32ビットSun JVMでどのように計算するかを学習しました.メモリがどのように組織されているかを知ると、クラスインスタンスが使用するメモリの数を理解するのに役立ちます.
 
次の記事では、関連内容をまとめ、反射(reflection)でオブジェクトのdeep sizeを計算するサンプルコードがあります.興味があれば、このソースの購読かこのブログの更新を待ってください!
 
日文原文:Code Instructions、翻訳:ImportNew -  鄭雯
訳文リンク:http://www.importnew.com/1305.html