Messen von Objektgrößen in der JVM

1. Übersicht

In diesem Tutorial werden wir sehen, wie viel Speicherplatz jedes Objekt im Java-Heap belegt.

Zunächst werden wir uns mit verschiedenen Metriken zur Berechnung der Objektgröße vertraut machen. Dann werden wir einige Möglichkeiten sehen, um die Instanzgrößen zu messen.

Normalerweise ist das Speicherlayout von Laufzeitdatenbereichen nicht Teil der JVM-Spezifikation und liegt im Ermessen des Implementierers. Daher kann jede JVM-Implementierung eine andere Strategie zum Layouten von Objekten und Arrays im Speicher haben. Dies wirkt sich wiederum auf die Instanzgrößen zur Laufzeit aus.

In diesem Tutorial konzentrieren wir uns auf eine bestimmte JVM-Implementierung: die HotSpot-JVM.

Wir verwenden auch die JVM- und HotSpot-JVM-Begriffe im gesamten Lernprogramm austauschbar.

2. Flache, beibehaltene und tiefe Objektgrößen

Zur Analyse der Objektgrößen können drei verschiedene Metriken verwendet werden: Flache, beibehaltene und tiefe Größen.

Bei der Berechnung der geringen Größe eines Objekts berücksichtigen wir nur das Objekt selbst. Das heißt, wenn das Objekt Verweise auf andere Objekte enthält, berücksichtigen wir nur die Referenzgröße für die Zielobjekte, nicht deren tatsächliche Objektgröße. Zum Beispiel:

Wie oben gezeigt, ist die geringe Größe der Triple- Instanz nur eine Summe von drei Referenzen. Wir schließen die tatsächliche Größe der referenzierten Objekte, nämlich A1, B1 und C1, von dieser Größe aus.

Im Gegenteil, die tiefe Größe eines Objekts umfasst zusätzlich zur flachen Größe die Größe aller referenzierten Objekte:

Hier enthält die tiefe Größe der Triple- Instanz drei Referenzen plus die tatsächliche Größe von A1, B1 und C1. Daher sind tiefe Größen rekursiver Natur.

Wenn der GC den von einem Objekt belegten Speicher zurückerobert, wird eine bestimmte Speichermenge freigegeben. Dieser Betrag entspricht der beibehaltenen Größe dieses Objekts:

Die beibehaltene Größe der Triple- Instanz umfasst neben der Triple- Instanz selbst nur A1 und C1 . Andererseits enthält diese beibehaltene Größe nicht B1, da die Pair- Instanz auch einen Verweis auf B1 enthält.

Manchmal werden diese zusätzlichen Verweise indirekt von der JVM selbst vorgenommen. Daher kann die Berechnung der beibehaltenen Größe eine komplizierte Aufgabe sein.

Um die beibehaltene Größe besser zu verstehen, sollten wir in Bezug auf die Speicherbereinigung denken. Durch das Sammeln der Triple- Instanz sind A1 und C1 nicht erreichbar, aber B1 ist weiterhin über ein anderes Objekt erreichbar. Abhängig von der Situation kann die beibehaltene Größe irgendwo zwischen der flachen und der tiefen Größe liegen.

3. Abhängigkeit

Um das Speicherlayout von Objekten oder Arrays in der JVM zu überprüfen, verwenden wir das Java Object Layout (JOL) -Tool. Daher müssen wir die Jol-Core- Abhängigkeit hinzufügen :

 org.openjdk.jol jol-core 0.10 

4. Einfache Datentypen

Um die Größe komplexerer Objekte besser verstehen zu können, sollten wir zunächst wissen, wie viel Speicherplatz jeder einfache Datentyp belegt. Dazu können wir das Java Memory Layout oder JOL bitten, die VM-Informationen zu drucken:

System.out.println(VM.current().details());

Mit dem obigen Code werden die einfachen Datentypgrößen wie folgt gedruckt:

# Running 64-bit HotSpot VM. # Using compressed oop with 3-bit shift. # Using compressed klass with 3-bit shift. # Objects are 8 bytes aligned. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Hier sind die Platzanforderungen für jeden einfachen Datentyp in der JVM:

  • Objektreferenzen verbrauchen 4 Bytes
  • Boolesche und Byte- Werte verbrauchen 1 Byte
  • Kurz- und Zeichenwerte verbrauchen 2 Bytes
  • int- und float- Werte verbrauchen 4 Bytes
  • lange und doppelte Werte verbrauchen 8 Bytes

Dies gilt sowohl für 32-Bit-Architekturen als auch für 64-Bit-Architekturen mit komprimierten Referenzen.

Erwähnenswert ist auch, dass alle Datentypen bei Verwendung als Array-Komponententypen dieselbe Speichermenge belegen.

4.1. Unkomprimierte Referenzen

Wenn wir die komprimierten Referenzen über das Tuning-Flag -XX: -UseCompressedOops deaktivieren , ändern sich die Größenanforderungen:

# Objects are 8 bytes aligned. # Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Jetzt verbrauchen Objektreferenzen 8 Bytes anstelle von 4 Bytes. Die verbleibenden Datentypen belegen immer noch dieselbe Speichermenge.

Darüber hinaus kann die HotSpot-JVM die komprimierten Referenzen auch nicht verwenden, wenn die Heap-Größe mehr als 32 GB beträgt (es sei denn, wir ändern die Objektausrichtung).

Unter dem Strich verbrauchen die Objektreferenzen 8 Byte, wenn wir die komprimierten Referenzen explizit deaktivieren oder die Heap-Größe mehr als 32 GB beträgt.

Nachdem wir den Speicherverbrauch für grundlegende Datentypen kennen, berechnen wir ihn für komplexere Objekte.

5. Komplexe Objekte

Um die Größe für komplexe Objekte zu berechnen, betrachten wir eine typische Beziehung zwischen Professor und Kurs:

public class Course { private String name; // constructor }

Jeder Professor kann zusätzlich zu den persönlichen Daten eine Liste der Kurse haben :

public class Professor { private String name; private boolean tenured; private List courses = new ArrayList(); private int level; private LocalDate birthDay; private double lastEvaluation; // constructor }

5.1. Geringe Größe: der Kurs Klasse

Die geringe Größe der Kurs Klasseninstanzen sollte einen 4-Byte - Objektverweis (für enthält Namen Feld) sowie einigen Objekt - Overhead. Wir können diese Annahme mit JOL überprüfen:

System.out.println(ClassLayout.parseClass(Course.class).toPrintable());

Dadurch wird Folgendes gedruckt:

Course object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 java.lang.String Course.name N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Wie oben gezeigt, ist die flache Größe 16 Bytes, 4 Bytes Objektreferenz auf den einschließlich Name Feld plus dem Objekt - Header.

5.2. Flache Größe: die Professor Klasse

Wenn wir denselben Code für die Professor- Klasse ausführen :

System.out.println(ClassLayout.parseClass(Professor.class).toPrintable());

Dann druckt JOL den Speicherverbrauch für die Professor- Klasse wie folgt:

Professor object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Professor.level N/A 16 8 double Professor.lastEvaluation N/A 24 1 boolean Professor.tenured N/A 25 3 (alignment/padding gap) 28 4 java.lang.String Professor.name N/A 32 4 java.util.List Professor.courses N/A 36 4 java.time.LocalDate Professor.birthDay N/A Instance size: 40 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

Wie wir wahrscheinlich erwartet haben, verbrauchen die gekapselten Felder 25 Bytes:

  • Drei Objektreferenzen, von denen jede 4 Bytes verbraucht. Also insgesamt 12 Bytes für den Verweis auf andere Objekte
  • Ein int, das 4 Bytes verbraucht
  • Ein Boolescher Wert, der 1 Byte verbraucht
  • Ein Double, das 8 Bytes verbraucht

Addiert man den 12-Byte-Overhead des Objekt-Headers plus 3 Byte Alignment-Padding, beträgt die flache Größe 40 Byte.

The key takeaway here is, in addition to the encapsulated state of each object, we should consider the object header and alignment paddings when calculating different object sizes.

5.3. Shallow Size: an Instance

The sizeOf() method in JOL provides a much simpler way to compute the shallow size of an object instance. If we run the following snippet:

String ds = "Data Structures"; Course course = new Course(ds); System.out.println("The shallow size is: " + VM.current().sizeOf(course));

It'll print the shallow size as follows:

The shallow size is: 16

5.4. Uncompressed Size

If we disable the compressed references or use more than 32 GB of the heap, the shallow size will increase:

Professor object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 8 double Professor.lastEvaluation N/A 24 4 int Professor.level N/A 28 1 boolean Professor.tenured N/A 29 3 (alignment/padding gap) 32 8 java.lang.String Professor.name N/A 40 8 java.util.List Professor.courses N/A 48 8 java.time.LocalDate Professor.birthDay N/A Instance size: 56 bytes Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

When the compressed references are disabled, the object header and object references will consume more memory. Therefore, as shown above, now the same Professor class consumes 16 more bytes.

5.5. Deep Size

To calculate the deep size, we should include the full size of the object itself and all of its collaborators. For instance, for this simple scenario:

String ds = "Data Structures"; Course course = new Course(ds);

The deep size of the Course instance is equal to the shallow size of the Course instance itself plus the deep size of that particular String instance.

With that being said, let's see how much space that String instance consumes:

System.out.println(ClassLayout.parseInstance(ds).toPrintable());

Each String instance encapsulates a char[] (more on this later) and an int hashcode:

java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) da 02 00 f8 12 4 char[] String.value [D, a, t, a, , S, t, r, u, c, t, u, r, e, s] 16 4 int String.hash 0 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The shallow size of this String instance is 24 bytes, which include the 4 bytes of cached hash code, 4 bytes of char[] reference, and other typical object overhead.

To see the actual size of the char[], we can parse its class layout, too:

System.out.println(ClassLayout.parseInstance(ds.toCharArray()).toPrintable());

The layout of the char[] looks like this:

[C object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 4 4 (object header) 00 00 00 00 8 4 (object header) 41 00 00 f8 12 4 (object header) 0f 00 00 00 16 30 char [C. N/A 46 2 (loss due to the next object alignment) Instance size: 48 bytes Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

So, we have 16 bytes for the Course instance, 24 bytes for the String instance, and finally 48 bytes for the char[]. In total, the deep size of that Course instance is 88 bytes.

With the introduction of compact strings in Java 9, the String class is internally using a byte[] to store the characters:

java.lang.String object internals: OFFSET SIZE TYPE DESCRIPTION 0 4 (object header) 4 4 (object header) 8 4 (object header) 12 4 byte[] String.value # the byte array 16 4 int String.hash 20 1 byte String.coder # encodig 21 3 (loss due to the next object alignment)

Therefore, on Java 9+, the total footprint of the Course instance will be 72 bytes instead of 88 bytes.

5.6. Object Graph Layout

Instead of parsing the class layout of each object in an object graph separately, we can use the GraphLayout. With GraphLayot, we just pass the starting point of the object graph, and it'll report the layout of all reachable objects from that starting point. This way, we can calculate the deep size of the starting point of the graph.

For instance, we can see the total footprint of the Course instance as follows:

System.out.println(GraphLayout.parseInstance(course).toFootprint());

Which prints the following summary:

[email protected] footprint: COUNT AVG SUM DESCRIPTION 1 48 48 [C 1 16 16 com.baeldung.objectsize.Course 1 24 24 java.lang.String 3 88 (total)

That's 88 bytes in total. The totalSize() method returns the total footprint of the object, which is 88 bytes:

System.out.println(GraphLayout.parseInstance(course).totalSize());

6. Instrumentation

To calculate the shallow size of an object, we can also use the Java instrumentation package and Java agents. First, we should create a class with a premain() method:

public class ObjectSizeCalculator { private static Instrumentation instrumentation; public static void premain(String args, Instrumentation inst) { instrumentation = inst; } public static long sizeOf(Object o) { return instrumentation.getObjectSize(o); } }

As shown above, we'll use the getObjectSize() method to find the shallow size of an object. We also need a manifest file:

Premain-Class: com.baeldung.objectsize.ObjectSizeCalculator

Then using this MANIFEST.MF file, we can create a JAR file and use it as a Java agent:

$ jar cmf MANIFEST.MF agent.jar *.class

Finally, if we run any code with the -javaagent:/path/to/agent.jar argument, then we can use the sizeOf() method:

String ds = "Data Structures"; Course course = new Course(ds); System.out.println(ObjectSizeCalculator.sizeOf(course));

This will print 16 as the shallow size of the Course instance.

7. Class Stats

To see the shallow size of objects in an already running application, we can take a look at the class stats using the jcmd:

$ jcmd  GC.class_stats [output_columns]

For instance, we can see each instance size and number of all the Course instances:

$ jcmd 63984 GC.class_stats InstSize,InstCount,InstBytes | grep Course 63984: InstSize InstCount InstBytes ClassName 16 1 16 com.baeldung.objectsize.Course

Again, this is reporting the shallow size of each Course instance as 16 bytes.

To see the class stats, we should launch the application with the -XX:+UnlockDiagnosticVMOptions tuning flag.

8. Heap Dump

Using heap dumps is another option to inspect the instance sizes in running applications. This way, we can see the retained size for each instance. To take a heap dump, we can use the jcmd as the following:

$ jcmd  GC.heap_dump [options] /path/to/dump/file

For instance:

$ jcmd 63984 GC.heap_dump -all ~/dump.hpro

This will create a heap dump in the specified location. Also, with the -all option, all reachable and unreachable objects will be present in the heap dump. Without this option, the JVM will perform a full GC before creating the heap dump.

After getting the heap dump, we can import it into tools like Visual VM:

As shown above, the retained size of the only Course instance is 24 bytes. As mentioned earlier, the retained size can be anywhere between shallow (16 bytes) and deep sizes (88 bytes).

It's also worth mentioning that the Visual VM was part of the Oracle and Open JDK distributions before Java 9. However, this is no longer the case as of Java 9, and we should download the Visual VM from its website separately.

9. Conclusion

In diesem Tutorial haben wir uns mit verschiedenen Metriken zum Messen der Objektgröße in der JVM-Laufzeit vertraut gemacht. Danach haben wir die Instanzgrößen tatsächlich mit verschiedenen Tools wie JOL, Java Agents und dem Befehlszeilenprogramm jcmd gemessen .

Wie üblich sind alle Beispiele auf GitHub verfügbar.