非正規化モデル
それでは、次のコードを見てみましょう。
class Cursor { String icon; Position pos; Cursor(String icon, int x, int y) { this.icon = icon; this.pos = new Position(x, y); } } class Position { int x; int y; Position(int x, int y) { this.x = x; this.y = y; } }
では、非正規化してみましょう。
class Cursor2 { String icon; int x; int y; Cursor2(String icon, int x, int y) { this.icon = icon; this.x = x; this.y = y; } }
それは思われる-組成物とすべてを取り除きました。 しかし、違います。 クラスCursor2のオブジェクトは、クラスCursorのオブジェクト(本質的にカーソル+位置)よりも約30%少ないメモリを消費します。 これは、分解の明らかな結果ではありません。 リンクと余分なオブジェクトのヘッダーが原因です。 おそらくこれは重要で面白くないように思えますが、オブジェクトがほとんどない限り、そして法案が数百万に達すると、状況は劇的に変わります。 これは、100フィールドの巨大なクラスを作成するための呼び出しではありません。 決して。 これは、RAMの上限に近づき、メモリ内に同じタイプの多くのオブジェクトがある場合にのみ役立ちます。
有利なオフセットを使用します
2つのクラスがあるとしましょう:
class A { int a; } class B { int a; int b; }
クラスAとクラスBのオブジェクトは同じ量のメモリを消費します。 ここでは、3つの結論を一度に引き出すことができます。
- 「クラスに別のフィールドを追加するか、外出先で保存して計算する価値があるか」と考える状況があります。 メモリがまったく節約されない場合、メモリを節約するためにCPU時間を犠牲にするのは愚かなことです。
- メモリを無駄にせずにフィールドを追加できる場合もありますが、計算またはキャッシュ用の追加または中間データをフィールドに保存できます(たとえば、Stringクラスのハッシュフィールド)。
- 場合によっては、intの代わりにbyteを使用しても意味がない場合があります。これは、位置合わせのために差を均等化できるためです。
プリミティブとシェル
もう一度繰り返します。 ただし、クラスでフィールドがnull値を受け入れないか、受け入れられない場合は、プリミティブを安全に使用します。 非常に頻繁に何かのようなものだから:
class A { @NotNull private Boolean isNew; @NotNull private Integer year; }
覚えておいて、プリミティブは平均で4分の1のメモリしか消費しません。 1つの整数フィールドをintに置き換えると、オブジェクトごとに16バイトのメモリが節約されます。 また、1つのLongをlongに置き換えると20バイトになります。 ガベージコレクターの負荷も軽減されます。 一般的に、多くの利点。 唯一の価格は、null値がないことです。 そして、状況によっては、メモリが本当に必要な場合、特定の値をヌル値として使用できます。 ただし、これには追加が必要になる場合があります。 アプリケーションロジックの修正コスト。
ブールおよびブール
これらの2つのタイプを選択します。 問題は、これらがjavaで最も神秘的な型であることです。 サイズは仕様で定義されていないため、論理型のサイズはJVMに完全に依存します。 Oracle HotSpot JVMについては、すべての論理型に4バイトが割り当てられています。つまり、intと同じです。 1ビットの情報を保存するには、ブール値の場合に31ビットを支払います。 ブール配列について話すと、ほとんどのコンパイラは何らかの最適化を実行します。この場合、ブールは値ごとに1バイトを占有します(BitSetを忘れないでください)。
そして最後に-ブール型を使用しないでください。 本当に必要な状況を思い付くのは難しいです。 ブールの場合のように、メモリの観点からははるかに安価であり、ビジネスロジックの観点からは、3ではなく2つの可能な値を取るプリミティブを使用する方が簡単です。
シリアライゼーションとデシリアライゼーション
シリアル化されたアプリケーションモデルがあり、ディスク上で1 GBを使用するとします。 そして、あなたの仕事はこのモデルをメモリに復元することです-単にそれを逆シリアル化します。 モデルの構造によっては、メモリ内で2GBから5GBを占有するという事実に備える必要があります。 はい、はい、同じヘッダー、オフセット、およびリンクのためにすべて再び。 したがって、リソースファイルに大量のデータを含めると便利な場合があります。 しかし、これはもちろん状況に非常に依存しており、これが常に出口であるとは限らず、時には不可能な場合もあります。
注文事項
2つの配列があるとします。
Object[2][1000] Object[1000][2]
それは思われる-違いはありません。 しかし、実際にはそうではありません...メモリ消費の観点から見ると、その差は非常に大きいです。 最初のケースでは、数千の要素の配列への2つの参照があります。 2番目のケースでは、2つの要素を持つ配列への1000の参照があります! メモリーの観点から見ると、2番目のケースでは、消費されるメモリーの量は998以上のリンクサイズです。 そして、これは約7kbです。 そのため、突然、多くのメモリを失う可能性があります。
リンク圧縮
Javaオブジェクトのリンク、見出し、およびオフセットで使用されるメモリを削減する機会があります。 大事なことは、32ビットアーキテクチャから64ビットアーキテクチャに移行したとき、多くの管理者、開発者だけが仮想Javaマシンのパフォーマンスの低下に気づいたことです。 さらに、移行中にアプリケーションが消費するメモリは、ビジネスモデルの構造に応じて20〜50%増加しました。 もちろん、それは彼らを動揺させるしかありませんでした。 移行の理由は明らかです-アプリケーションは、32ビットアーキテクチャの使用可能なアドレス空間に収まりません。 誰も知らない-32ビットシステムでは、メモリセルへのポインタのサイズ(1バイト)は32ビットを使用します。 したがって、32ビットポインターが使用できる最大使用可能メモリは2 ^ 32 = 4294967296バイトまたは4 GBです。 しかし、実際のアプリケーションでは、アドレス空間の一部がインストールされた周辺機器(ビデオカードなど)に使用されるため、4 GBのボリュームには到達できません。
Java開発者は途方に暮れておらず、リンク圧縮などがありました。 通常、javaのリンクのサイズはネイティブシステムのサイズと同じです。 64ビットアーキテクチャでは64ビットです。 つまり、実際には2 ^ 64個のオブジェクトを参照できます。 しかし、このような膨大な数のポインターは不要です。 そのため、仮想マシンの開発者はリンクのサイズを節約することを決定し、オプション-XX:+ UseCompressedOopsを導入しました。 このオプションは、64ビットJVMのポインターのサイズを32ビットに削減しました。 これにより何が得られますか?
- リンクを持つすべてのオブジェクトは、リンクごとに4バイト少なくなります。
- 各オブジェクトのタイトルは4バイト削減されます。
- 状況によっては、アライメントを減らすことができます。
- 消費されるメモリの量は大幅に削減されます。
しかし、2つの小さなマイナス点があります。
- 可能なオブジェクトの数は2 ^ 32にあります。 この項目をマイナスと呼ぶことはほとんどありません。 同意すると、40億個のオブジェクトが非常に多くなります。 また、オブジェクトの最小サイズが16バイトであることも考慮して...
- 余分に表示されます。 JVMリンクをネイティブに、またはその逆に変換するコスト。 これらが文字通り2つのレジスター操作であるシフトと加算を考えると、これらのコストが少なくとも何らかの形で実際にパフォーマンスに影響を与えることは疑わしいです。 詳細はこちらをご覧ください。
UseCompressedOopsオプションに多くのプラスとマイナスがほとんどない場合、多くの人が疑問を持っていると確信しています。なぜデフォルトでオンになっていないのですか? 実際、JDK 6 update 23以降では、JDK 7と同様にデフォルトで有効になっています。また、最初にupdate 6pで登場しました。
おわりに
私はあなたを説得することができたと思います。 実際のプロジェクトでこれらのトリックのいくつかを見る機会がありました。 ドナルド・クヌースがかつて言っていたように、時期尚早な最適化がすべての病気の根源であることを忘れないでください。