Javaのヒント130:データサイズを知っていますか?

最近、私はインメモリデータベースに似たJavaサーバーアプリケーションの設計を手伝いました。つまり、超高速のクエリパフォーマンスを提供するために、大量のデータをメモリにキャッシュするように設計をバイアスしました。

プロトタイプを実行したら、当然、データメモリフットプリントを解析してディスクからロードした後、プロファイルを作成することにしました。しかし、最初の結果が不十分だったため、説明を探すようになりました。

注:この記事のソースコードは、リソースからダウンロードできます。

ツール

Javaはメモリ管理の多くの側面を意図的に隠しているため、オブジェクトが消費するメモリの量を見つけるには、ある程度の作業が必要です。このRuntime.freeMemory()メソッドを使用して、複数のオブジェクトが割り当てられる前後のヒープサイズの違いを測定できます。 RamchanderVaradarajanの「Questionofthe Week No. 107」(Sun Microsystems、2000年9月)やTony Sintesの「MemoryMatters」(JavaWorld、 2001年12月)など、いくつかの記事でそのアイデアが詳しく説明されています。残念ながら、前者の記事のソリューションは実装が間違ったRuntime方法を採用しているため失敗しますが、後者の記事のソリューションには独自の欠陥があります。

  • Runtime.freeMemory()JVMはいつでも(特にガベージコレクションを実行するときに)現在のヒープサイズを増やすことを決定する可能性があるため、への1回の呼び出しでは不十分であることがわかります。合計ヒープサイズがすでに-Xmxの最大サイズになっていないRuntime.totalMemory()-Runtime.freeMemory()限り、使用するヒープサイズとして使用する必要があります。
  • 1回のRuntime.gc()呼び出しを実行しても、ガベージコレクションを要求するには十分に積極的ではない場合があります。たとえば、オブジェクトファイナライザーの実行を要求することもできます。また、Runtime.gc()収集が完了するまでブロックするように文書化されていないため、認識されるヒープサイズが安定するまで待つことをお勧めします。
  • プロファイルクラスがクラスごとのクラス初期化(静的クラスおよびフィールド初期化子を含む)の一部として静的データを作成する場合、ファーストクラスインスタンスに使用されるヒープメモリにそのデータが含まれる場合があります。ファーストクラスのインスタンスによって消費されるヒープスペースは無視する必要があります。

これらの問題を考慮してSizeof、さまざまなJavaコアおよびアプリケーションクラスをスヌープするためのツールを紹介します。

public class Sizeof {public static void main(String [] args)throws Exception {//使用するすべてのクラス/メソッドをウォームアップしますrunGC(); usedMemory(); //割り当てられたオブジェクトへの強力な参照を保持する配列finalint count = 100000;オブジェクト[]オブジェクト=新しいオブジェクト[カウント];長いヒープ1 = 0; // count + 1オブジェクトを割り当て、(int i = -1; i = 0)オブジェクトの最初のオブジェクトを破棄します[i] = object; else {オブジェクト= null; //ウォームアップオブジェクトを破棄しますrunGC(); heap1 = usedMemory(); //ヒープ前のスナップショットを作成します}} runGC(); long heap2 = usedMemory(); //ヒープ後のスナップショットを作成します:final int size = Math.round(((float)(heap2 --heap1))/ count); System.out.println( "'before' heap:" + heap1 + "、 'after' heap:" + heap2); System.out.println( "ヒープデルタ:" +(heap2-heap1)+ "、{" +オブジェクト[0]。getClass()+ "} size =" + size + "bytes"); for(int i = 0; i <count; ++ i)オブジェクト[i] = null;オブジェクト= null; } private static void runGC()throws Exception {// Runtime.gc()を呼び出すのに役立ちます//いくつかのメソッド呼び出しを使用します:for(int r = 0; r <4; ++ r)_runGC(); } private static void _runGC()throws Exception {long usedMem1 = usedMemory()、usedMem2 = Long.MAX_VALUE; for(int i = 0;(usedMem1 <usedMem2)&&(i <500); ++ i){s_runtime.runFinalization(); s_runtime.gc(); Thread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりi <カウント; ++ i)オブジェクト[i] = null;オブジェクト= null; } private static void runGC()throws Exception {// Runtime.gc()を呼び出すのに役立ちます//いくつかのメソッド呼び出しを使用します:for(int r = 0; r <4; ++ r)_runGC(); } private static void _runGC()throws Exception {long usedMem1 = usedMemory()、usedMem2 = Long.MAX_VALUE; for(int i = 0;(usedMem1 <usedMem2)&&(i <500); ++ i){s_runtime.runFinalization(); s_runtime.gc(); Thread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりi <カウント; ++ i)オブジェクト[i] = null;オブジェクト= null; } private static void runGC()throws Exception {// Runtime.gc()を呼び出すのに役立ちます//いくつかのメソッド呼び出しを使用します:for(int r = 0; r <4; ++ r)_runGC(); } private static void _runGC()throws Exception {long usedMem1 = usedMemory()、usedMem2 = Long.MAX_VALUE; for(int i = 0;(usedMem1 <usedMem2)&&(i <500); ++ i){s_runtime.runFinalization(); s_runtime.gc(); Thread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりgc()//いくつかのメソッド呼び出しを使用する:for(int r = 0; r <4; ++ r)_runGC(); } private static void _runGC()throws Exception {long usedMem1 = usedMemory()、usedMem2 = Long.MAX_VALUE; for(int i = 0;(usedMem1 <usedMem2)&&(i <500); ++ i){s_runtime.runFinalization(); s_runtime.gc(); Thread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりgc()//いくつかのメソッド呼び出しを使用する:for(int r = 0; r <4; ++ r)_runGC(); } private static void _runGC()throws Exception {long usedMem1 = usedMemory()、usedMem2 = Long.MAX_VALUE; for(int i = 0;(usedMem1 <usedMem2)&&(i <500); ++ i){s_runtime.runFinalization(); s_runtime.gc(); Thread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりThread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わりThread.currentThread()。yield(); usedMem2 = usedMem1; usedMem1 = usedMemory(); }} private static long usedMemory(){return s_runtime.totalMemory()-s_runtime.freeMemory(); } private static final Runtime s_runtime = Runtime.getRuntime(); } //クラスの終わり

Sizeof主要な方法があるrunGC()usedMemory()。メソッドをより積極的にするように見えるので、runGC()ラッパーメソッドを使用して_runGC()数回呼び出します。(理由はわかりませんが、メソッド呼び出しスタックフレームを作成および破棄すると、到達可能性ルートセットが変更され、ガベージコレクターの作業がさらに困難になる可能性があります。さらに、十分な作業を作成するためにヒープスペースの大部分を消費します。ガベージコレクターが起動することも役立ちます。一般に、すべてが確実に収集されるようにすることは困難です。正確な詳細は、JVMとガベージコレクションアルゴリズムによって異なります。)

を呼び出す場所に注意してくださいrunGC()heap1heap2宣言の間のコードを編集して、関心のあるものをインスタンス化できます。

またSizeof、オブジェクトサイズを出力する方法にも注意してください。すべてのcountクラスインスタンスに必要なデータの推移閉包をcount。で割ったものです。ほとんどのクラスでは、結果は、所有するすべてのフィールドを含む、単一のクラスインスタンスによって消費されるメモリになります。そのメモリフットプリント値は、浅いメモリフットプリントを報告する多くの商用プロファイラーによって提供されるデータとは異なります(たとえば、オブジェクトにint[]フィールドがある場合、そのメモリ消費量は個別に表示されます)。

結果

この単純なツールをいくつかのクラスに適用して、結果が期待どおりかどうかを確認してみましょう。

注:以下の結果は、SunのJDK 1.3.1 forWindowsに基づいています。Java言語およびJVM仕様によって保証されているものと保証されていないものがあるため、これらの特定の結果を他のプラットフォームまたは他のJava実装に適用することはできません。

java.lang.Object

さて、すべてのオブジェクトのルートは私の最初のケースでなければなりませんでした。の場合java.lang.Object、次のようになります。

'前'ヒープ:510696、 '後'ヒープ:1310696ヒープデルタ:800000、{クラスjava.lang.Object}サイズ= 8バイト 

したがって、プレーンObjectは8バイトかかります。もちろん、誰もがすべてのインスタンスとして、サイズが0であることを期待してはならない支持台操作のようなことをフィールドを持ち歩く必要がありequals()hashCode()wait()/notify()、など。

java.lang.Integer

同僚と私は頻繁にネイティブintsIntegerインスタンスにラップして、Javaコレクションに保存できるようにします。メモリ内でいくらかかりますか?

'前'ヒープ:510696、 '後'ヒープ:2110696ヒープデルタ:1600000、{クラスjava.lang.Integer}サイズ= 16バイト 

16バイトの結果は、int値が4バイト余分に収まる可能性があるため、予想よりも少し悪くなります。Integer値をプリミティブ型として格納できる場合と比較して、コストを使用すると、300%のメモリオーバーヘッドが発生します。

java.lang.Long

Longより多くのメモリを必要としますがInteger、そうではありません。

'前'ヒープ:510696、 '後'ヒープ:2110696ヒープデルタ:1600000、{クラスjava.lang.Long}サイズ= 16バイト 

明らかに、ヒープ上の実際のオブジェクトサイズは、特定のCPUタイプの特定のJVM実装によって実行される低レベルのメモリアライメントの影響を受けます。aLongは8バイトのObjectオーバーヘッドに加えて、実際の長い値の場合はさらに8バイトのように見えます。対照的にInteger、未使用の4バイトの穴がありました。これは、使用するJVMが8バイトのワード境界でオブジェクトの整列を強制するためと考えられます。

配列

プリミティブ型の配列で遊ぶことは、一部は隠れたオーバーヘッドを発見し、一部は別の一般的なトリックを正当化するために有益であることがわかります。プリミティブ値をサイズ1の配列にラップして、オブジェクトとして使用します。Sizeof.main()反復ごとに作成された配列の長さをインクリメントするループを持つように変更することで、配列を取得しintます。

長さ:0、{クラス[I}サイズ= 16バイト長さ:1、{クラス[I}サイズ= 16バイト長さ:2、{クラス[I}サイズ= 24バイト長さ:3、{クラス[I}サイズ= 24バイト長:4、{クラス[I}サイズ= 32バイト長:5、{クラス[I}サイズ= 32バイト長:6、{クラス[I}サイズ= 40バイト長:7、{クラス[I}サイズ= 40バイト長さ:8、{クラス[I}サイズ= 48バイト長さ:9、{クラス[I}サイズ= 48バイト長さ:10、{クラス[I}サイズ= 56バイト 

およびcharアレイの場合:

長さ:0、{クラス[C}サイズ= 16バイト長さ:1、{クラス[C}サイズ= 16バイト長さ:2、{クラス[C}サイズ= 16バイト長さ:3、{クラス[C}サイズ= 24バイト長:4、{クラス[C}サイズ= 24バイト長:5、{クラス[C}サイズ= 24バイト長:6、{クラス[C}サイズ= 24バイト長:7、{クラス[C}サイズ= 32バイト長:8、{クラス[C}サイズ= 32バイト長:9、{クラス[C}サイズ= 32バイト長:10、{クラス[C}サイズ= 32バイト 

上記では、8バイトのアライメントの証拠が再び表示されます。また、避けられないObject8バイトのオーバーヘッドに加えて、プリミティブ配列はさらに8バイトを追加します(そのうち少なくとも4バイトがlengthフィールドをサポートします)。また、同じデータの変更可能なバージョンを除いint[1]て、使用してもIntegerインスタンスに比べてメモリの利点はないようです。

多次元配列

Multidimensional arrays offer another surprise. Developers commonly employ constructs like int[dim1][dim2] in numerical and scientific computing. In an int[dim1][dim2] array instance, every nested int[dim2] array is an Object in its own right. Each adds the usual 16-byte array overhead. When I don't need a triangular or ragged array, that represents pure overhead. The impact grows when array dimensions greatly differ. For example, a int[128][2] instance takes 3,600 bytes. Compared to the 1,040 bytes an int[256] instance uses (which has the same capacity), 3,600 bytes represent a 246 percent overhead. In the extreme case of byte[256][1], the overhead factor is almost 19! Compare that to the C/C++ situation in which the same syntax does not add any storage overhead.

java.lang.String

Let's try an empty String, first constructed as new String():

'before' heap: 510696, 'after' heap: 4510696 heap delta: 4000000, {class java.lang.String} size = 40 bytes 

The result proves quite depressing. An empty String takes 40 bytes—enough memory to fit 20 Java characters.

Before I try Strings with content, I need a helper method to create Strings guaranteed not to get interned. Merely using literals as in:

 object = "string with 20 chars"; 

will not work because all such object handles will end up pointing to the same String instance. The language specification dictates such behavior (see also the java.lang.String.intern() method). Therefore, to continue our memory snooping, try:

 public static String createString (final int length) { char [] result = new char [length]; for (int i = 0; i < length; ++ i) result [i] = (char) i; return new String (result); } 

After arming myself with this String creator method, I get the following results:

length: 0, {class java.lang.String} size = 40 bytes length: 1, {class java.lang.String} size = 40 bytes length: 2, {class java.lang.String} size = 40 bytes length: 3, {class java.lang.String} size = 48 bytes length: 4, {class java.lang.String} size = 48 bytes length: 5, {class java.lang.String} size = 48 bytes length: 6, {class java.lang.String} size = 48 bytes length: 7, {class java.lang.String} size = 56 bytes length: 8, {class java.lang.String} size = 56 bytes length: 9, {class java.lang.String} size = 56 bytes length: 10, {class java.lang.String} size = 56 bytes 

The results clearly show that a String's memory growth tracks its internal char array's growth. However, the String class adds another 24 bytes of overhead. For a nonempty String of size 10 characters or less, the added overhead cost relative to useful payload (2 bytes for each char plus 4 bytes for the length), ranges from 100 to 400 percent.

Of course, the penalty depends on your application's data distribution. Somehow I suspected that 10 characters represents the typical String length for a variety of applications. To get a concrete data point, I instrumented the SwingSet2 demo (by modifying the String class implementation directly) that came with JDK 1.3.x to track the lengths of the Strings it creates. After a few minutes playing with the demo, a data dump showed that about 180,000 Strings were instantiated. Sorting them into size buckets confirmed my expectations:

[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ... 

That's right, more than 50 percent of all String lengths fell into the 0-10 bucket, the very hot spot of String class inefficiency!

実際には、Stringsは長さが示唆するよりもさらに多くのメモリを消費する可能性があります。sからString生成されたStringBuffer(明示的にまたは「+」連結演算子を介して)sは、通常16の容量で始まるためchar、報告Stringされた長さよりも長い配列を持っている可能性があります。StringBuffer、その後、append()操作でそれを2倍にします。だから、例えば、createString(1) + ' 'で終わるcharサイズ16の配列ではなく、2。

私たちは何をしますか?

「これはすべて非常にうまくいっていStringますが、Javaが提供するsやその他のタイプを使用する以外に選択肢はありませんか?」あなたが尋ねるのを聞きます。確認してみましょう。

ラッパークラス