Java-Tipp 130: Kennen Sie Ihre Datengröße?

Kürzlich half ich beim Entwerfen einer Java-Serveranwendung, die einer In-Memory-Datenbank ähnelte. Das heißt, wir haben das Design darauf ausgerichtet, Tonnen von Daten im Speicher zwischenzuspeichern, um eine superschnelle Abfrageleistung zu erzielen.

Nachdem wir den Prototyp zum Laufen gebracht hatten, beschlossen wir natürlich, den Speicherbedarf des Datenspeichers zu analysieren, nachdem er analysiert und von der Festplatte geladen wurde. Die unbefriedigenden ersten Ergebnisse veranlassten mich jedoch, nach Erklärungen zu suchen.

Hinweis: Sie können den Quellcode dieses Artikels von Resources herunterladen.

Das Werkzeug

Da Java viele Aspekte der Speicherverwaltung absichtlich verbirgt, ist es einige Arbeit, herauszufinden, wie viel Speicher Ihre Objekte verbrauchen. Mit dieser Runtime.freeMemory()Methode können Sie Heap-Größenunterschiede messen, bevor und nachdem mehrere Objekte zugewiesen wurden. Mehrere Artikel, wie Ramchander Varadarajans "Frage der Woche Nr. 107" (Sun Microsystems, September 2000) und Tony Sintes '"Memory Matters" ( JavaWorld, Dezember 2001), beschreiben diese Idee detailliert. Leider schlägt die Lösung des ersteren Artikels fehl, weil die Implementierung eine falsche RuntimeMethode verwendet, während die Lösung des letzteren Artikels ihre eigenen Mängel aufweist:

  • Ein einzelner Aufruf von Runtime.freeMemory()erweist sich als unzureichend, da eine JVM jederzeit entscheiden kann, ihre aktuelle Heap-Größe zu erhöhen (insbesondere, wenn die Garbage Collection ausgeführt wird). Sofern die Gesamtgröße des Heapspeichers nicht bereits die maximale Größe von -Xmx hat, sollten wir sie Runtime.totalMemory()-Runtime.freeMemory()als Größe des verwendeten Heapspeichers verwenden.
  • Das Ausführen eines einzelnen Runtime.gc()Aufrufs ist möglicherweise nicht aggressiv genug, um die Speicherbereinigung anzufordern. Wir könnten zum Beispiel auch Objekt-Finalisierer zum Ausführen auffordern. Und da Runtime.gc()nicht dokumentiert ist, dass es blockiert wird, bis die Sammlung abgeschlossen ist, ist es eine gute Idee zu warten, bis sich die wahrgenommene Heap-Größe stabilisiert.
  • Wenn die profilierte Klasse im Rahmen ihrer Klasseninitialisierung pro Klasse statische Daten erstellt (einschließlich statischer Klassen- und Feldinitialisierer), kann der für die erste Klasseninstanz verwendete Heapspeicher diese Daten enthalten. Wir sollten den von der First-Class-Instanz belegten Heap-Speicherplatz ignorieren.

In Anbetracht dieser Probleme Sizeofstelle ich ein Tool vor, mit dem ich verschiedene Java-Kern- und Anwendungsklassen beschnüffle:

öffentliche Klasse Sizeof {public static void main (String [] args) löst eine Ausnahme aus {// Erwärme alle Klassen / Methoden, die wir verwenden werden runGC (); verwendeter Speicher (); // Array, um starke Verweise auf zugewiesene Objekte beizubehalten final int count = 100000; Objekt [] Objekte = neues Objekt [Anzahl]; langer Haufen1 = 0; // Anzahl + 1 Objekte zuweisen, das erste für (int i = -1; i = 0) Objekte verwerfen [i] = Objekt; sonst {object = null; // Verwerfe das Aufwärmobjekt runGC (); heap1 = usedMemory (); // Mach einen Schnappschuss vor dem Heap}} runGC (); long heap2 = usedMemory (); // Machen Sie einen Snapshot nach dem Heap: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'vor' Heap:" + Heap1 + ", 'nach' Heap:" + Heap2); System.out.println ("Heap-Delta:" + (Heap2 - Heap1) + ", {" + Objekte [0].getClass () + "} size =" + size + "bytes"); für (int i = 0; i <count; ++ i) Objekte [i] = null; Objekte = null; } private static void runGC () löst eine Ausnahme aus {// Es hilft, Runtime.gc () // mit mehreren Methodenaufrufen aufzurufen: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () löst eine Ausnahme aus {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; für (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 (); } // Ende des Unterrichtsi <count; ++ i) Objekte [i] = null; Objekte = null; } private static void runGC () löst eine Ausnahme aus {// Es hilft, Runtime.gc () // mit mehreren Methodenaufrufen aufzurufen: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () löst eine Ausnahme aus {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; für (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 (); } // Ende des Unterrichtsi <count; ++ i) Objekte [i] = null; Objekte = null; } private static void runGC () löst eine Ausnahme aus {// Es hilft, Runtime.gc () // mit mehreren Methodenaufrufen aufzurufen: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () löst eine Ausnahme aus {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; für (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 (); } // Ende des Unterrichtsgc () // unter Verwendung mehrerer Methodenaufrufe: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () löst eine Ausnahme aus {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; für (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 (); } // Ende des Unterrichtsgc () // unter Verwendung mehrerer Methodenaufrufe: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () löst eine Ausnahme aus {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; für (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 (); } // Ende des UnterrichtsThread.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 (); } // Ende des UnterrichtsThread.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 (); } // Ende des Unterrichts

SizeofDie wichtigsten Methoden sind runGC()und usedMemory(). Ich benutze eine runGC()Wrapper-Methode, um _runGC()mehrmals aufzurufen, weil sie die Methode aggressiver zu machen scheint. (Ich bin nicht sicher, warum, aber es ist möglich, dass das Erstellen und Zerstören eines Methodenaufruf-Stack-Frames zu einer Änderung des Erreichbarkeitsstammsatzes führt und den Garbage Collector dazu veranlasst, härter zu arbeiten. Außerdem wird ein großer Teil des Heapspeichers verbraucht, um genügend Arbeit zu erstellen Es hilft auch, wenn der Garbage Collector aktiv wird. Im Allgemeinen ist es schwierig sicherzustellen, dass alles erfasst wird. Die genauen Details hängen von der JVM und dem Garbage Collection-Algorithmus ab.)

Notieren Sie sorgfältig die Stellen, an denen ich anrufe runGC(). Sie können den Code zwischen den Deklarationen heap1und und bearbeiten heap2, um alles Interessante zu instanziieren.

Beachten Sie auch, wie Sizeofdie Objektgröße gedruckt wird: das transitive Schließen von Daten, die von allen countKlasseninstanzen benötigt werden , geteilt durch count. Für die meisten Klassen wird das Ergebnis Speicher sein, der von einer einzelnen Klasseninstanz einschließlich aller ihrer eigenen Felder belegt wird. Dieser Wert für den Speicherbedarf unterscheidet sich von den Daten, die von vielen kommerziellen Profilern bereitgestellt werden, die einen geringen Speicherbedarf melden (wenn ein Objekt beispielsweise ein int[]Feld hat, wird sein Speicherverbrauch separat angezeigt).

Die Ergebnisse

Wenden wir dieses einfache Tool auf einige Klassen an und prüfen Sie, ob die Ergebnisse unseren Erwartungen entsprechen.

Hinweis: Die folgenden Ergebnisse basieren auf Suns JDK 1.3.1 für Windows. Aufgrund dessen, was durch die Java-Sprache und die JVM-Spezifikationen garantiert wird und was nicht, können Sie diese spezifischen Ergebnisse nicht auf andere Plattformen oder andere Java-Implementierungen anwenden.

java.lang.Object

Nun, die Wurzel aller Objekte musste nur mein erster Fall sein. Denn java.lang.Objectich bekomme:

'vor' Heap: 510696, 'nach' Heap: 1310696 Heap Delta: 800000, {Klasse java.lang.Object} Größe = 8 Bytes 

Eine Ebene Objectbenötigt also 8 Bytes. erwarten sollte natürlich niemand die Größe 0 sein, da jede Instanz herumtragen müssen Felder , dass die Unterstützung Basisoperationen wie equals(), hashCode(), wait()/notify(), und so weiter.

java.lang.Integer

Meine Kollegen und ich verpacken native häufig intsin IntegerInstanzen, damit wir sie in Java-Sammlungen speichern können. Wie viel kostet es uns im Gedächtnis?

'vor' Heap: 510696, 'nach' Heap: 2110696 Heap Delta: 1600000, {Klasse java.lang.Integer} Größe = 16 Bytes 

Das 16-Byte-Ergebnis ist etwas schlechter als ich erwartet hatte, da ein intWert in nur 4 zusätzliche Bytes passen kann. Die Verwendung von a Integerkostet mich 300 Prozent Speicheraufwand im Vergleich dazu, wenn ich den Wert als primitiven Typ speichern kann.

java.lang.Long

Longsollte mehr Speicher benötigen als Integer, aber es tut nicht:

'vor' Heap: 510696, 'nach' Heap: 2110696 Heap Delta: 1600000, {Klasse java.lang.Long} Größe = 16 Bytes 

Es ist klar, dass die tatsächliche Objektgröße auf dem Heap einer Speicherausrichtung auf niedriger Ebene unterliegt, die von einer bestimmten JVM-Implementierung für einen bestimmten CPU-Typ durchgeführt wird. Es sieht so aus, als ob a Long8 Byte ObjectOverhead plus 8 Byte mehr für den tatsächlichen Long-Wert ist. Im Gegensatz dazu Integerhatte ein unbenutztes 4-Byte-Loch, höchstwahrscheinlich, weil die JVM, die ich verwende, die Objektausrichtung an einer 8-Byte-Wortgrenze erzwingt.

Arrays

Das Spielen mit primitiven Arrays erweist sich als lehrreich, teils um versteckten Overhead zu entdecken, teils um einen anderen beliebten Trick zu rechtfertigen: das Umschließen primitiver Werte in ein Array der Größe 1, um sie als Objekte zu verwenden. Durch Ändern Sizeof.main()einer Schleife, die die erstellte Array-Länge bei jeder Iteration erhöht, erhalte ich für intArrays:

Länge: 0, {Klasse [I} Größe = 16 Byte Länge: 1, {Klasse [I} Größe = 16 Byte Länge: 2, {Klasse [I} Größe = 24 Byte Länge: 3, {Klasse [I} Größe = 24 Byte Länge: 4, {Klasse [I} Größe = 32 Byte Länge: 5, {Klasse [I} Größe = 32 Byte Länge: 6, {Klasse [I} Größe = 40 Byte Länge: 7, {Klasse [I} Größe = 40 Byte Länge: 8, {Klasse [I} Größe = 48 Byte Länge: 9, {Klasse [I} Größe = 48 Byte Länge: 10, {Klasse [I} Größe = 56 Byte 

und für charArrays:

Länge: 0, {Klasse [C} Größe = 16 Byte Länge: 1, {Klasse [C} Größe = 16 Byte Länge: 2, {Klasse [C} Größe = 16 Byte Länge: 3, {Klasse [C} Größe = 24 Byte Länge: 4, {Klasse [C} Größe = 24 Byte Länge: 5, {Klasse [C} Größe = 24 Byte Länge: 6, {Klasse [C} Größe = 24 Byte Länge: 7, {Klasse [C} Größe = 32 Byte Länge: 8, {Klasse [C} Größe = 32 Byte Länge: 9, {Klasse [C} Größe = 32 Byte Länge: 10, {Klasse [C} Größe = 32 Byte 

Oben wird der Hinweis auf eine 8-Byte-Ausrichtung erneut angezeigt. Zusätzlich zum unvermeidlichen Object8-Byte-Overhead fügt ein primitives Array weitere 8 Bytes hinzu (von denen mindestens 4 Bytes das lengthFeld unterstützen). Und die Verwendung int[1]scheint keine Speichervorteile gegenüber einer IntegerInstanz zu bieten , außer möglicherweise als veränderbare Version derselben Daten.

Mehrdimensionale Arrays

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!

In der Realität kann Strings sogar noch mehr Speicher belegen, als ihre Längen vermuten lassen: Strings, die aus StringBuffers (entweder explizit oder über den Verkettungsoperator '+') generiert wurden, haben wahrscheinlich charArrays mit Längen, die größer als die angegebenen StringLängen sind, da StringBuffers normalerweise mit einer Kapazität von 16 beginnen , dann verdoppeln Sie es bei append()Operationen. So entsteht beispielsweise createString(1) + ' 'ein charArray der Größe 16, nicht 2.

Was machen wir?

"Das ist alles sehr gut, aber wir haben keine andere Wahl, als Strings und andere von Java bereitgestellte Typen zu verwenden , oder?" Ich höre dich fragen. Lass es uns herausfinden.

Wrapper-Klassen