Misurazione delle dimensioni degli oggetti nella JVM

1. Panoramica

In questo tutorial, vedremo quanto spazio consuma ogni oggetto nell'heap Java.

Innanzitutto, acquisiremo familiarità con diverse metriche per calcolare le dimensioni degli oggetti. Quindi, vedremo alcuni modi per misurare le dimensioni delle istanze.

Di solito, il layout di memoria delle aree dati di runtime non fa parte della specifica JVM ed è lasciato alla discrezione dell'implementatore. Pertanto, ciascuna implementazione JVM può avere una strategia diversa per il layout di oggetti e array in memoria. Ciò, a sua volta, influenzerà le dimensioni dell'istanza in fase di esecuzione.

In questo tutorial, ci stiamo concentrando su un'implementazione JVM specifica: HotSpot JVM.

Usiamo anche i termini JVM e HotSpot JVM in modo intercambiabile durante il tutorial.

2. Dimensioni degli oggetti poco profonde, trattenute e profonde

Per analizzare le dimensioni degli oggetti, possiamo utilizzare tre diverse metriche: dimensioni superficiali, mantenute e profonde.

Quando si calcola la dimensione superficiale di un oggetto, si considera solo l'oggetto stesso. Cioè, se l'oggetto ha riferimenti ad altri oggetti, consideriamo solo la dimensione di riferimento agli oggetti di destinazione, non la loro dimensione effettiva dell'oggetto. Per esempio:

Come mostrato sopra, la dimensione superficiale dell'istanza Triple è solo una somma di tre riferimenti. Escludiamo la dimensione effettiva degli oggetti indicati, vale a dire A1, B1 e C1, da questa dimensione.

Al contrario, la dimensione profonda di un oggetto include la dimensione di tutti gli oggetti di riferimento, oltre alla dimensione superficiale:

Qui la dimensione profonda dell'istanza Triple contiene tre riferimenti più la dimensione effettiva di A1, B1 e C1. Pertanto, le dimensioni profonde sono di natura ricorsiva.

Quando il GC recupera la memoria occupata da un oggetto, libera una quantità specifica di memoria. Quella quantità è la dimensione trattenuta di quell'oggetto:

La dimensione mantenuta dell'istanza Triple include solo A1 e C1 oltre all'istanza Triple stessa. D'altra parte, questa dimensione mantenuta non include B1, poiché anche l' istanza di Pair ha un riferimento a B1.

A volte questi riferimenti extra vengono creati indirettamente dalla stessa JVM. Pertanto, il calcolo della dimensione mantenuta può essere un'operazione complicata.

Per comprendere meglio la dimensione mantenuta, dovremmo pensare in termini di garbage collection. La raccolta dell'istanza Tripla rende A1 e C1 irraggiungibili, ma B1 è ancora raggiungibile attraverso un altro oggetto. A seconda della situazione, la dimensione mantenuta può essere ovunque tra la dimensione superficiale e quella profonda.

3. Dipendenza

Per ispezionare il layout di memoria di oggetti o array nella JVM, utilizzeremo lo strumento JOL (Java Object Layout). Pertanto, dovremo aggiungere la dipendenza jol-core :

 org.openjdk.jol jol-core 0.10 

4. Tipi di dati semplici

Per avere una migliore comprensione della dimensione di oggetti più complessi, dobbiamo prima sapere quanto spazio consuma ogni semplice tipo di dati. Per fare ciò, possiamo chiedere a Java Memory Layout o JOL di stampare le informazioni sulla VM:

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

Il codice sopra stamperà le dimensioni del tipo di dati semplice come segue:

# 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]

Quindi ecco i requisiti di spazio per ogni semplice tipo di dati nella JVM:

  • I riferimenti agli oggetti consumano 4 byte
  • I valori booleani e byte consumano 1 byte
  • i valori short e char consumano 2 byte
  • I valori int e float consumano 4 byte
  • i valori long e double consumano 8 byte

Questo è vero nelle architetture a 32 bit e anche nelle architetture a 64 bit con riferimenti compressi attivi.

Vale anche la pena ricordare che tutti i tipi di dati consumano la stessa quantità di memoria quando vengono utilizzati come tipi di componenti dell'array.

4.1. Riferimenti non compressi

Se disabilitiamo i riferimenti compressi tramite -XX: -UseCompressedOops flag di ottimizzazione, i requisiti di dimensione cambieranno:

# 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]

Ora i riferimenti agli oggetti consumeranno 8 byte invece di 4 byte. I restanti tipi di dati consumano ancora la stessa quantità di memoria.

Inoltre, la JVM HotSpot non può utilizzare i riferimenti compressi quando la dimensione dell'heap è superiore a 32 GB (a meno che non modifichiamo l'allineamento dell'oggetto).

La conclusione è che se disabilitiamo esplicitamente i riferimenti compressi o la dimensione dell'heap è superiore a 32 GB, i riferimenti agli oggetti consumeranno 8 byte.

Ora che conosciamo il consumo di memoria per i tipi di dati di base, calcoliamolo per oggetti più complessi.

5. Oggetti complessi

Per calcolare la dimensione di oggetti complessi, consideriamo una tipica relazione tra professore e corso:

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

Ogni Docente, oltre ai dati anagrafici, può avere un elenco dei Corsi :

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. Dimensioni poco profonde: la classe del corso

La dimensione ridotta delle istanze della classe Course dovrebbe includere un riferimento a un oggetto a 4 byte (per il campo del nome ) più un sovraccarico di oggetti. Possiamo verificare questa ipotesi usando JOL:

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

Questo stamperà quanto segue:

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

Come mostrato sopra, la dimensione superficiale è di 16 byte, incluso un riferimento a un oggetto di 4 byte al campo del nome più l'intestazione dell'oggetto.

5.2. Dimensioni poco profonde: la classe del professore

Se eseguiamo lo stesso codice per la classe Professor :

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

Quindi JOL stamperà il consumo di memoria per la classe Professore come segue:

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

Come probabilmente ci aspettavamo, i campi incapsulati consumano 25 byte:

  • Tre riferimenti a oggetti, ognuno dei quali consuma 4 byte. Quindi 12 byte in totale per fare riferimento ad altri oggetti
  • Un int che consuma 4 byte
  • Un booleano che consuma 1 byte
  • Un doppio che consuma 8 byte

Aggiungendo il sovraccarico di 12 byte dell'intestazione dell'oggetto più 3 byte di riempimento dell'allineamento, la dimensione superficiale è di 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 questo tutorial, abbiamo acquisito familiarità con diverse metriche per misurare le dimensioni degli oggetti nel runtime JVM. Successivamente, abbiamo effettivamente misurato le dimensioni delle istanze con vari strumenti come JOL, Java Agent e l' utilità della riga di comando jcmd .

Come al solito, tutti gli esempi sono disponibili su GitHub.