JavaのSizeof

2003年12月26日

Q: JavaにはCのsizeof()のような演算子がありますか?

A:表面的な答えは、JavaはCのようなものを提供しないということsizeof()です。ただし、Javaプログラマーが時々それを必要とする理由を考えてみましょう。

ACプログラマは、ほとんどのデータ構造メモリ割り当てを自分で管理し、sizeof()割り当てるメモリブロックサイズを知るために不可欠です。さらに、のようなCメモリアロケータmalloc()は、オブジェクトの初期化に関する限り、ほとんど何もしません。プログラマは、他のオブジェクトへのポインタであるすべてのオブジェクトフィールドを設定する必要があります。しかし、すべてを言い、コーディングすると、C / C ++のメモリ割り当ては非常に効率的です。

比較すると、Javaオブジェクトの割り当てと構築は結びついています(割り当てられているが初期化されていないオブジェクトインスタンスを使用することはできません)。 Javaクラスが他のオブジェクトへの参照であるフィールドを定義する場合、構築時にそれらを設定することも一般的です。したがって、Javaオブジェクトを割り当てると、相互接続された多数のオブジェクトインスタンス(オブジェクトグラフ)が頻繁に割り当てられます。自動ガベージコレクションと組み合わせると、これは非常に便利であり、Javaメモリ割り当ての詳細について心配する必要がないように感じることができます。

もちろん、これは単純なJavaアプリケーションでのみ機能します。C / C ++と比較して、同等のJavaデータ構造はより多くの物理メモリを占有する傾向があります。エンタープライズソフトウェア開発では、今日の32ビットJVMで使用可能な最大仮想メモリに近づくことは、一般的なスケーラビリティの制約です。したがって、Javaプログラマーはsizeof()、データ構造が大きくなりすぎているのか、メモリーのボトルネックが含まれているのかを監視することで、その恩恵を受けることができます。幸い、Javaリフレクションを使用すると、このようなツールを非常に簡単に作成できます。

先に進む前に、この記事の質問に対する頻繁ではあるが間違った回答を省きます。

誤謬:Java基本型のサイズが固定されているため、Sizeof()は必要ありません

はい、JavaintはすべてのJVMおよびすべてのプラットフォームで32ビットですが、これは、このデータ型のプログラマーが認識できる幅の言語仕様要件にすぎません。このようなanintは本質的に抽象データ型であり、たとえば64ビットマシン上の64ビット物理メモリワードによってバックアップできます。非プリミティブ型についても同じことが言えます。Java言語仕様では、クラスフィールドを物理メモリでどのように整列させるか、またはブール値の配列をJVM内でコンパクトなビットベクトルとして実装できないことについては何も述べられていません。

誤謬:オブジェクトをバイトストリームにシリアル化し、結果のストリーム長を確認することで、オブジェクトのサイズを測定できます。

これが機能しない理由は、シリアル化レイアウトが実際のメモリ内レイアウトのリモート反映にすぎないためです。それを確認する簡単な方法の1つは、Stringsがどのようにシリアル化されるかを調べることです。メモリ内ではすべてcharが少なくとも2バイトですが、シリアル化された形式ではStringsはUTF-8でエンコードされるため、ASCIIコンテンツは半分のスペースを必要とします。

別の実用的なアプローチ

「Javaのヒント130:データサイズを知っていますか?」を思い出すかもしれません。多数の同一のクラスインスタンスを作成し、JVMで使用されるヒープサイズの結果として生じる増加を注意深く測定することに基づく手法について説明しました。該当する場合、このアイデアは非常にうまく機能します。実際、このアイデアを使用して、この記事の代替アプローチをブートストラップします。

Java Tip 130のSizeofクラスは静止JVMを必要とし(ヒープアクティビティは測定スレッドによって要求されたオブジェクト割り当てとガベージコレクションのみによるものであるため)、多数の同一のオブジェクトインスタンスを必要とすることに注意してください。これは、単一の大きなオブジェクトのサイズを設定する場合(おそらく、デバッグトレース出力の一部として)、特に実際にそれを非常に大きくした理由を調べたい場合には機能しません。

オブジェクトのサイズはどれくらいですか?

上記の説明は、哲学的なポイントを強調しています。通常、オブジェクトグラフを扱う場合、オブジェクトサイズの定義は何ですか。調べているオブジェクトインスタンスのサイズだけですか、それともオブジェクトインスタンスをルートとするデータグラフ全体のサイズですか?後者は、通常、実際にはもっと重要なことです。ご覧のとおり、物事は必ずしも明確ではありませんが、初心者の場合は、次のアプローチに従うことができます。

  • オブジェクトインスタンスは、そのすべての非静的データフィールド(スーパークラスで定義されたフィールドを含む)を合計することにより、(おおよその)サイズを設定できます。
  • たとえば、C ++とは異なり、クラスメソッドとその仮想性はオブジェクトサイズに影響を与えません。
  • クラススーパーインターフェイスは、オブジェクトサイズに影響を与えません(このリストの最後にある注を参照してください)
  • 完全なオブジェクトサイズは、開始オブジェクトをルートとするオブジェクトグラフ全体のクロージャとして取得できます。
注: Javaインターフェースを実装すると、問題のクラスにマークが付けられるだけで、その定義にデータが追加されることはありません。実際、JVMは、インターフェースの実装がインターフェースに必要なすべてのメソッドを提供することを検証していません。これは、現在の仕様では厳密にコンパイラーの責任です。

プロセスをブートストラップするために、プリミティブデータ型の場合、Java Tip130のSizeofクラスで測定された物理サイズを使用します。実は、一般的な32ビットJVMの場合、プレーンjava.lang.Objectは8バイトを使用し、基本的なデータ型は通常、言語要件に対応できる最小の物理サイズです(boolean1バイト全体を使用する場合を除く)。

// java.lang.Objectシェルサイズ(バイト単位):public static final int OBJECT_SHELL_SIZE = 8; public static final int OBJREF_SIZE = 4; public static final int LONG_FIELD_SIZE = 8; public static final int INT_FIELD_SIZE = 4; public static final int SHORT_FIELD_SIZE = 2; public static final int CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; public static final int BOOLEAN_FIELD_SIZE = 1; public static final int DOUBLE_FIELD_SIZE = 8; public static final int FLOAT_FIELD_SIZE = 4;

(これらの定数は永久にハードコーディングされておらず、特定のJVMに対して個別に測定する必要があることを理解することが重要です。)もちろん、オブジェクトフィールドサイズの単純な合計は、JVMのメモリアライメントの問題を無視します。メモリの配置は重要ですが(たとえば、Java Tip 130のプリミティブ配列型で示されているように)、そのような低レベルの詳細を追跡することは不利益だと思います。このような詳細はJVMベンダーに依存しているだけでなく、プログラマーの管理下にはありません。私たちの目的は、オブジェクトのサイズを適切に推測し、クラスフィールドが冗長である可能性がある場合に手がかりを得ることです。または、フィールドに遅延を入力する必要がある場合。または、よりコンパクトなネストされたデータ構造が必要な場合など。絶対的な物理的精度を得るには、いつでもSizeofJava Tip130のクラスに戻ることができます。

オブジェクトインスタンスを構成するもののプロファイルを作成するために、ツールはサイズを計算するだけでなく、副産物として役立つデータ構造を構築します。これは、次の要素で構成されるグラフIObjectProfileNodeです。

インターフェイスIObjectProfileNode {オブジェクトオブジェクト(); 文字列名(); intサイズ(); int refcount(); IObjectProfileNode親(); IObjectProfileNode []子(); IObjectProfileNodeシェル(); IObjectProfileNode []パス(); IObjectProfileNodeルート(); intパスの長さ(); ブールトラバース(INodeFilterフィルター、INodeVisitorビジター); 文字列ダンプ(); } //インターフェースの終わり

IObjectProfileNodeは、元のオブジェクトグラフとほぼ同じ方法で相互接続され、IObjectProfileNode.object()各ノードが表す実際のオブジェクトを返します。IObjectProfileNode.size()そのノードのオブジェクトインスタンスをルートとするオブジェクトサブツリーの合計サイズ(バイト単位)を返します。オブジェクトインスタンスがnull以外のインスタンスフィールドまたは配列フィールド内に含まれる参照を介して他のオブジェクトにリンクしている場合、IObjectProfileNode.children()サイズの降順で並べ替えられた、対応する子グラフノードのリストになります。逆に、開始ノード以外のすべてのノードについて、IObjectProfileNode.parent()その親を返します。IObjectProfileNodeしたがって、sのコレクション全体が元のオブジェクトをスライスおよびダイシングし、データストレージがその中でどのように分割されているかを示します。さらに、グラフノード名はクラスフィールドから導出され、グラフ内のノードのパスを調べます(IObjectProfileNode.path())は、元のオブジェクトインスタンスから内部データへの所有権リンクを追跡できます。

前の段落を読んでいるときに、これまでのアイデアにはまだあいまいさが残っていることに気付いたかもしれません。オブジェクトグラフをトラバースしているときに、同じオブジェクトインスタンスに複数回遭遇した場合(つまり、グラフのどこかで複数のフィールドがそれを指している場合)、その所有権(親ポインター)をどのように割り当てますか?このコードスニペットを検討してください。

 Object obj = new String [] {new String( "JavaWorld")、new String( "JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

グラフトラバーサルと最短パスについて考えると、この時点でベルが鳴るはずです。幅優先探索は、開始ノードから他の到達可能なグラフノードまでの最短パスを見つけることを保証するグラフトラバーサルアルゴリズムです。

これらすべての予備知識の後で、ここにそのようなグラフ走査の教科書実装があります。(一部の詳細と補助的な方法は省略されています。詳細については、この記事のダウンロードを参照してください。):