Mesure de la taille des objets dans la JVM

1. Vue d'ensemble

Dans ce tutoriel, nous allons voir combien d'espace chaque objet consomme dans le tas Java.

Tout d'abord, nous nous familiariserons avec différentes métriques pour calculer la taille des objets. Ensuite, nous allons voir quelques façons de mesurer la taille des instances.

Habituellement, la disposition de la mémoire des zones de données d'exécution ne fait pas partie de la spécification JVM et est laissée à la discrétion de l'implémenteur. Par conséquent, chaque implémentation JVM peut avoir une stratégie différente pour mettre en page les objets et les tableaux en mémoire. Cela affectera à son tour les tailles d'instance au moment de l'exécution.

Dans ce tutoriel, nous nous concentrons sur une implémentation JVM spécifique: la JVM HotSpot.

Nous utilisons également les termes JVM et HotSpot JVM de manière interchangeable tout au long du didacticiel.

2. Tailles d'objets peu profonds, retenus et profonds

Pour analyser les tailles des objets, nous pouvons utiliser trois mesures différentes: les tailles peu profondes, conservées et profondes.

Lors du calcul de la taille superficielle d'un objet, nous ne considérons que l'objet lui-même. Autrement dit, si l'objet a des références à d'autres objets, nous ne considérons que la taille de référence des objets cibles, pas leur taille réelle d'objet. Par exemple:

Comme indiqué ci-dessus, la taille superficielle de l' instance Triple n'est qu'une somme de trois références. Nous excluons la taille réelle des objets référencés, à savoir A1, B1 et C1, de cette taille.

Au contraire, la taille profonde d'un objet comprend la taille de tous les objets référencés, en plus de la taille peu profonde:

Ici, la taille profonde de l' instance Triple contient trois références plus la taille réelle de A1, B1 et C1. Par conséquent, les tailles profondes sont de nature récursive.

Lorsque le GC récupère la mémoire occupée par un objet, il libère une quantité spécifique de mémoire. Ce montant correspond à la taille conservée de cet objet:

La taille conservée de l' instance Triple inclut uniquement A1 et C1 en plus de l' instance Triple elle-même. D'autre part, cette taille conservée n'inclut pas le B1, puisque l' instance Pair a également une référence à B1.

Parfois, ces références supplémentaires sont indirectement faites par la JVM elle-même. Par conséquent, le calcul de la taille conservée peut être une tâche compliquée.

Pour mieux comprendre la taille conservée, nous devons penser en termes de ramasse-miettes. La collecte de l' instance Triple rend les A1 et C1 inaccessibles, mais le B1 est toujours accessible via un autre objet. Selon la situation, la taille retenue peut être n'importe où entre la taille peu profonde et la taille profonde.

3. Dépendance

Pour inspecter la disposition de la mémoire des objets ou des tableaux dans la JVM, nous allons utiliser l'outil Java Object Layout (JOL). Par conséquent, nous devrons ajouter la dépendance jol-core :

 org.openjdk.jol jol-core 0.10 

4. Types de données simples

Pour avoir une meilleure compréhension de la taille des objets plus complexes, nous devons d'abord savoir combien d'espace chaque type de données simple consomme. Pour ce faire, nous pouvons demander à Java Memory Layout ou JOL d'imprimer les informations de la VM:

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

Le code ci-dessus imprimera les tailles de type de données simples comme suit:

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

Voici donc l'espace requis pour chaque type de données simple dans la machine virtuelle Java:

  • Les références d'objet consomment 4 octets
  • Les valeurs booléennes et octets consomment 1 octet
  • les valeurs short et char consomment 2 octets
  • Les valeurs int et float consomment 4 octets
  • les valeurs longues et doubles consomment 8 octets

Cela est vrai dans les architectures 32 bits et aussi dans les architectures 64 bits avec des références compressées en vigueur.

Il convient également de mentionner que tous les types de données consomment la même quantité de mémoire lorsqu'ils sont utilisés comme types de composants de tableau.

4.1. Références non compressées

Si nous désactivons les références compressées via -XX: -UseCompressedOops tuning flag, alors les exigences de taille changeront:

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

Désormais, les références d'objet consomment 8 octets au lieu de 4 octets. Les types de données restants consomment toujours la même quantité de mémoire.

De plus, la JVM HotSpot ne peut pas non plus utiliser les références compressées lorsque la taille du tas est supérieure à 32 Go (sauf si nous modifions l'alignement des objets).

L'essentiel est que si nous désactivons les références compressées explicitement ou si la taille du tas est supérieure à 32 Go, les références d'objet consommeront 8 octets.

Maintenant que nous connaissons la consommation de mémoire pour les types de données de base, calculons-la pour des objets plus complexes.

5. Objets complexes

Pour calculer la taille d'objets complexes, considérons une relation professeur-cours typique:

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

Chaque professeur, en plus des détails personnels, peut avoir une liste de cours :

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. Petite taille: la classe de cours

La taille superficielle des instances de classe Course doit inclure une référence d'objet de 4 octets (pour le champ de nom ) plus une surcharge d'objet. Nous pouvons vérifier cette hypothèse en utilisant JOL:

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

Cela imprimera ce qui suit:

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

Comme indiqué ci-dessus, la taille superficielle est de 16 octets, y compris une référence d'objet de 4 octets au champ de nom plus l'en-tête de l'objet.

5.2. Petite taille: la classe professeur

If we run the same code for the Professor class:

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

Then JOL will print the memory consumption for the Professor class like the following:

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

As we probably expected, the encapsulated fields are consuming 25 bytes:

  • Three object references, each of which consumes 4 bytes. So 12 bytes in total for referring to other objects
  • One int which consumes 4 bytes
  • One boolean which consumes 1 byte
  • One double which consumes 8 bytes

Adding the 12 bytes overhead of the object header plus 3 bytes of alignment padding, the shallow size is 40 bytes.

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

Dans ce didacticiel, nous nous sommes familiarisés avec différentes métriques pour mesurer la taille des objets dans l'environnement d'exécution JVM. Après cela, nous avons mesuré la taille des instances avec divers outils tels que JOL, Java Agents et l' utilitaire de ligne de commande jcmd .

Comme d'habitude, tous les exemples sont disponibles sur over sur GitHub.